Module: Sisimai::SMTP::Status

Defined in:
lib/sisimai/smtp/status.rb

Overview

Sisimai::RFC3463 is utilities for getting D.S.N. value from error reason text, getting the reason from D.S.N. value, and getting D.S.N. from the text including D.S.N.

Constant Summary collapse

CodePatterns =
[
  %r/[ ]?[(][#]([45][.]\d[.]\d+)[)]?[ ]?/,  # #5.5.1
  %r/\b\d{3}[- ][#]?([45][.]\d[.]\d+)\b/,   # 550-5.1.1 OR 550 5.5.1
  %r/\b([45][.]\d[.]\d+)\b/,                # 5.5.1
  %r/\b(2[.][0-7][.][0-7])\b/,              # 2.1.5
]
StandardCode =
{
  '2.1.5'  => 'delivered',      # Successfully delivered
  # ---------------------------------------------------------------------------------------
  '4.1.6'  => 'hasmoved',       # Destination mailbox has moved, No forwarding address
  '4.1.7'  => 'rejected',       # Bad sender's mailbox address syntax
  '4.1.8'  => 'rejected',       # Bad sender's system address
  '4.1.9'  => 'systemerror',    # Message relayed to non-compliant mailer
  '4.2.1'  => 'suspend',        # Mailbox disabled, not accepting messages
  '4.2.2'  => 'mailboxfull',    # Mailbox full
  '4.2.3'  => 'emailtoolarge',  # Message length exceeds administrative limit
  '4.2.4'  => 'systemerror',    # Mailing list expansion problem
  # '4.3.0' => 'systemerror',    # Other or undefined mail system status
  '4.3.1'  => 'systemfull',     # Mail system full
  '4.3.2'  => 'notaccept',      # System not accepting network messages
  '4.3.3'  => 'systemerror',    # System not capable of selected features
  '4.3.5'  => 'systemerror',    # System incorrectly configured
  # '4.4.0' => 'networkerror',   # Other or undefined network or routing status
  '4.4.1'  => 'expired',        # No answer from host
  '4.4.2'  => 'networkerror',   # Bad connection
  '4.4.3'  => 'systemerror',    # Directory server failure
  '4.4.4'  => 'networkerror',   # Unable to route
  '4.4.5'  => 'systemfull',     # Mail system congestion
  '4.4.6'  => 'networkerror',   # Routing loop detected
  '4.4.7'  => 'expired',        # Delivery time expired
  '4.4.8'  => 'networkerror',   # Retry on IPv4
#         '4.5.0'  => 'networkerror',   # Other or undefined protocol status
  '4.5.3'  => 'ratelimited',    # Too many recipients
  '4.5.5'  => 'systemerror',    # Wrong protocol version
  '4.6.0'  => 'contenterror',   # Other or undefined media error
  '4.6.2'  => 'contenterror',   # Conversion required and prohibited
  '4.6.5'  => 'contenterror',   # Conversion Failed
  # :'4.7.0' => 'securityerror',  # Other or undefined security status
  '4.7.1'  => 'blocked',        # Delivery not authorized, message refused
  '4.7.2'  => 'rejected',       # Mailing list expansion prohibited
  '4.7.5'  => 'securityerror',  # Cryptographic failure
  '4.7.6'  => 'securityerror',  # Cryptographic algorithm not supported
  '4.7.7'  => 'securityerror',  # Message integrity failure
  '4.7.12' => 'securityerror',  # A password transition is needed
  '4.7.15' => 'securityerror',  # Priority Level is too low
  '4.7.16' => 'emailtoolarge',  # Message is too big for the specified priority
  '4.7.24' => 'authfailure ',   # SPF validation error
  '4.7.25' => 'requireptr',     # Reverse DNS validation failed
  # ---------------------------------------------------------------------------------------
  '5.1.0'  => 'userunknown',    # Other address status
  '5.1.1'  => 'userunknown',    # Bad destination mailbox address
  '5.1.2'  => 'hostunknown',    # Bad destination system address
  '5.1.3'  => 'userunknown',    # Bad destination mailbox address syntax
  '5.1.4'  => 'filtered',       # Destination mailbox address ambiguous
  '5.1.6'  => 'hasmoved',       # Destination mailbox has moved, No forwarding address
  '5.1.7'  => 'rejected',       # Bad sender's mailbox address syntax
  '5.1.8'  => 'rejected',       # Bad sender's system address
  '5.1.9'  => 'systemerror',    # Message relayed to non-compliant mailer
  '5.1.10' => 'notaccept',      # Recipient address has null MX
  '5.2.0'  => 'filtered',       # Other or undefined mailbox status
  '5.2.1'  => 'filtered',       # Mailbox disabled, not accepting messages
  '5.2.2'  => 'mailboxfull',    # Mailbox full
  '5.2.3'  => 'emailtoolarge',  # Message length exceeds administrative limit
  '5.2.4'  => 'systemerror',    # Mailing list expansion problem
  '5.3.0'  => 'systemerror',    # Other or undefined mail system status
  '5.3.1'  => 'systemfull',     # Mail system full
  '5.3.2'  => 'notaccept',      # System not accepting network messages
  '5.3.3'  => 'systemerror',    # System not capable of selected features
  '5.3.4'  => 'emailtoolarge',  # Message too big for system
  '5.3.5'  => 'systemerror',    # System incorrectly configured
  '5.4.0'  => 'networkerror',   # Other or undefined network or routing status
  '5.4.3'  => 'systemerror',    # Directory server failure
  '5.4.4'  => 'hostunknown',    # Unable to route
  '5.5.2'  => 'systemerror',    # If the server cannot BASE64 decode any client response (AUTH)
  '5.5.3'  => 'ratelimited',    # Too many recipients
  '5.5.4'  => 'systemerror',    # Invalid command arguments
  '5.5.5'  => 'systemerror',    # Wrong protocol version
  '5.5.6'  => 'syntaxerror',    # Authentication Exchange line is too long
  '5.6.0'  => 'contenterror',   # Other or undefined media error
  '5.6.1'  => 'contenterror',   # Media not supported
  '5.6.2'  => 'contenterror',   # Conversion required and prohibited
  '5.6.3'  => 'contenterror',   # Conversion required but not supported
  '5.6.5'  => 'contenterror',   # Conversion Failed
  '5.6.6'  => 'contenterror',   # Message content not available
  '5.6.7'  => 'rejected',       # Non-ASCII addresses not permitted for that sender/recipient
  '5.6.8'  => 'contenterror',   # UTF-8 string reply is required, but not permitted by the SMTP client
  '5.6.9'  => 'contenterror',   # UTF-8 header message cannot be transferred to one or more recipients
  '5.7.0'  => 'securityerror',  # Other or undefined security status
  '5.7.1'  => 'securityerror',  # Delivery not authorized, message refused
  '5.7.2'  => 'securityerror',  # Mailing list expansion prohibited
  '5.7.3'  => 'securityerror',  # Security conversion required but not possible
  '5.7.4'  => 'securityerror',  # Security features not supported
  '5.7.5'  => 'securityerror',  # Cryptographic failure
  '5.7.6'  => 'securityerror',  # Cryptographic algorithm not supported
  '5.7.7'  => 'securityerror',  # Message integrity failure
  '5.7.8'  => 'securityerror',  # Authentication credentials invalid
  '5.7.9'  => 'securityerror',  # Authentication mechanism is too weak
  '5.7.10' => 'securityerror',  # Encryption Needed
  '5.7.11' => 'securityerror',  # Encryption required for requested authentication mechanism
  '5.7.13' => 'suspend',        # User Account Disabled
  '5.7.14' => 'securityerror',  # Trust relationship required
  '5.7.15' => 'securityerror',  # Priority Level is too low
  '5.7.16' => 'emailtoolarge',  # Message is too big for the specified priority
  '5.7.17' => 'hasmoved',       # Mailbox owner has changed
  '5.7.18' => 'hasmoved',       # Domain owner has changed
  '5.7.19' => 'systemerror',    # RRVS test cannot be completed
  '5.7.20' => 'authfailure',    # No passing DKIM signature found
  '5.7.21' => 'authfailure',    # No acceptable DKIM signature found
  '5.7.22' => 'authfailure',    # No valid author-matched DKIM signature found
  '5.7.23' => 'authfailure',    # SPF validation failed
  '5.7.24' => 'authfailure',    # SPF validation error
  '5.7.25' => 'requireptr',     # Reverse DNS validation failed
  '5.7.26' => 'authfailure',    # Multiple authentication checks failed
  '5.7.27' => 'notaccept',      # MX resource record of a destination host is Null MX: RFC7505
  '5.7.28' => 'spamdetected',   # The message appears to be part of a mail flood of similar abusive messages.
  '5.7.29' => 'authfailure',    # This status code may be returned when a message fails ARC validation.
  '5.7.30' => 'failedstarttls', # REQUIRETLS support required
}.freeze
InternalCode =
{
  'authfailure'     => ['5.9.130', '4.9.130'],
  'badreputation'   => ['5.9.132', '4.9.132'],
  'blocked'         => ['5.9.134', '4.9.134'],
  'contenterror'    => ['5.9.160', '4.9.160'],
  'emailtoolarge'   => ['5.9.161', '4.9.161'],
  'expired'         => ['5.9.340', '4.9.340'],
  'failedstarttls'  => ['5.9.350', '4.9.350'],
  'filtered'        => ['5.9.210', '4.9.210'],
  'hasmoved'        => ['5.9.211', ''],
  'hostunknown'     => ['5.9.212', ''],
  'mailboxfull'     => ['5.9.220', '4.9.220'],
  'mailererror'     => ['5.9.230', '4.9.230'],
  'networkerror'    => ['5.9.341', '4.9.341'],
  'norelaying'      => ['5.9.214', '4.9.214'],
  'notaccept'       => ['5.9.215', '4.9.215'],
  'notcompliantrfc' => ['5.9.162', '4.9.162'],
  'onhold'          => ['5.9.301', '4.9.301'],
  'policyviolation' => ['5.9.371', '4.9.371'],
  'ratelimited'     => ['5.9.131', '4.9.131'],
  'rejected'        => ['5.9.110', '4.9.110'],
  'requireptr'      => ['5.9.133', '4.9.133'],
  'securityerror'   => ['5.9.370', '4.9.370'],
  'spamdetected'    => ['5.9.164', '4.9.164'],
  'suppressed'      => ['5.9.310', '4.9.310'],
  'suspend'         => ['5.9.221', '4.9.221'],
  'syntaxerror'     => ['5.9.351', '4.9.351'],
  'systemerror'     => ['5.9.231', '4.9.231'],
  'systemfull'      => ['5.9.232', '4.9.232'],
  'undefined'       => ['5.9.300', '4.9.300'],
  'userunknown'     => ['5.9.213', ''],
  'virusdetected'   => ['5.9.165', '4.9.165'],
}.freeze

Class Method Summary collapse

Class Method Details

.code(argv1 = "", argv2 = false) ⇒ String

Convert from the reason string to the internal status code

Parameters:

  • argv1 (String) (defaults to: "")

    Reason name

  • argv2 (True, False) (defaults to: false)

    false: Permanent error true: Temporary error

Returns:

  • (String)

    DSN or an empty string if the 1st argument is missing

See Also:



649
650
651
652
653
654
# File 'lib/sisimai/smtp/status.rb', line 649

def code(argv1 = "", argv2 = false)
  return "" if argv1.empty?

  pairs = InternalCode[argv1]; return "" if pairs.nil?
  return argv2 ? pairs[1] : pairs[0]
end

.find(argv1 = nil, argv2 = '0') ⇒ String

Get a DSN code value from given string including DSN

Parameters:

  • argv1 (String) (defaults to: nil)

    String including DSN

  • argv2 (String) (defaults to: '0')

    An SMTP Reply Code or 2 or 4 or 5

Returns:

  • (String)

    An SMTP Status Code or an empty string



686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
# File 'lib/sisimai/smtp/status.rb', line 686

def find(argv1 = nil, argv2 = '0')
  return "" if argv1.to_s.empty? || argv1.size < 7

  case argv2[0, 1]
    when '2', '4', '5' then eestatuses = [argv2[0, 1] + '.']
    else                    eestatuses = ['5.', '4.', '2.']
  end
  esmtperror = ' ' + argv1 + '   ' # Why 3 space characters? see https://github.com/sisimai/p5-sisimai/issues/574
  lookingfor = []

  Sisimai::RFC791.find(esmtperror).each do |e|
    # Rewrite an IPv4 address in the given string(argv1) with '***.***.***.***'
    p0 = esmtperror.index(e) || next
    esmtperror[p0, e.size] = '***.***.***.***'
  end

  eestatuses.each do |e|
    # Count the number of "5.", "4.", and "2." in the error message
    p0 = 0; p1 = 0
    while p0
      # Find all of the "5." and "4." string and store its postion
      p0 = esmtperror.index(e, p1) || break
      lookingfor << [p0, e]
      p1 = p0 + 5
    end
  end
  return "" if lookingfor.size == 0

  statuscode = []   # List of SMTP Status Code, Keep the order of appearances
  anotherone = ''   # Alternative code
  readbuffer = ''
  characters = []   # Characters around the status code found by index()
  indexofees = nil  # A position of SMTP status code found by the index()

  lookingfor.sort_by(&:first).each do |e|
    # Try to find an SMTP Status Code from the given string
    indexofees = esmtperror.index(e[1], e[0]); next if indexofees.nil?
    characters = [esmtperror[indexofees - 1, 1].ord]  # [0] The previous character of the status
    [2, 3].each do |i|
      # [1] The value of the "Subject", "5.[7].261"
      # [2] "." chacater, a separator of the Subject and the Detail
      if indexofees + 1 + i > esmtperror.size
        characters << 0
      else
        characters << esmtperror[indexofees + i, 1].ord
      end
    end

    next if characters[0]  > 45 && characters[0]  <  58 # Previous character is a number
    next if characters[0] == 86 || characters[0] == 118 # Avoid a version number("V" or "v")
    next if characters[1]  < 48 || characters[1]  >  55 # The value of the subject is not a number(0-7)
    next if characters[2] != 46                         # It is not a "." character: a separator
    readbuffer = e[1] + characters[1].chr + '.'

    [4, 5, 6, 7].each do |i|
      # [3] The 1st digit of the detail
      # [4] The 2nd digit of the detail
      # [5] The 3rd digit of the detail
      # [6] The next character
      if indexofees + 1 + i > esmtperror.size
        characters << 0
      else
        characters << esmtperror[indexofees + i, 1].ord
      end
    end

    next if characters[3] < 48 || characters[3] > 57  # The 1st digit of the detail is not a number
    readbuffer << characters[3].chr

    if Sisimai::SMTP::Status.is_ambiguous(readbuffer) || readbuffer == "4.4.7"
      # Find another status code except *.0.0, 4.4.7
      anotherone = readbuffer
      next
    end

    if characters[4] < 48 || characters[4] > 57
      # The 2nd digit of the detail is not a number
      statuscode << readbuffer
      next
    end
    readbuffer << characters[4].chr # The 2nd digit of the detail is a number

    if characters[5] < 48 || characters[5] > 57
      # The 3rd digit of the detail is not a number
      statuscode << readbuffer
      next
    end
    readbuffer << characters[5].chr # The 3rd digit of the detail is a number

    next if characters[6] > 47 && characters[6] < 58
    statuscode << readbuffer
  end
  statuscode << anotherone if anotherone.size > 0
  return "" if statuscode.size == 0

  # Select one from picked status codes
  cv = statuscode.shift; statuscode.each { |e| cv = Sisimai::SMTP::Status.prefer(cv, e, "") }

  return cv
end

.is_ambiguous(argv1 = '') ⇒ Object

is_ambiguous() returns true when the argument is not empty and ends with “.0.0”.

Parameters:

  • string

    argv1 Status code

Returns:

  • bool false: The delivery status code is not ambiguous



862
863
864
865
866
# File 'lib/sisimai/smtp/status.rb', line 862

def is_ambiguous(argv1 = '')
  return true if argv1.nil? || argv1.empty?
  return true if argv1.size == 5 && argv1.end_with?(".0.0")
  return false
end

.is_explicit(argv1 = '') ⇒ Object

is_explicit() returns false when the argument is empty or is an internal code

Parameters:

  • string

    argv1 Status code

Returns:

  • bool false: The delivery status code is not explicit



853
854
855
856
857
# File 'lib/sisimai/smtp/status.rb', line 853

def is_explicit(argv1 = '')
  return false if argv1.nil? || argv1.empty?
  return false if argv1.size == 7 && argv1.start_with?("5.9.", "4.9.")
  return true
end

.name(argv1 = nil) ⇒ String

Convert from the status code to the reason string

Parameters:

  • argv1 (String) (defaults to: nil)

    Status code(DSN)

Returns:

  • (String)

    Reason name or an empty string

See Also:



660
661
662
663
# File 'lib/sisimai/smtp/status.rb', line 660

def name(argv1 = nil)
  return "" if Sisimai::SMTP::Status.test(argv1.to_s) == false
  return StandardCode[argv1] || ""
end

.prefer(argv0 = nil, argv1 = nil, argv2 = nil) ⇒ String

Return the preferred value selected from the arguments

Parameters:

  • argv0 (String) (defaults to: nil)

    The value of Status:

  • argv1 (String) (defaults to: nil)

    The delivery status picked from the error message

  • argv2 (String) (defaults to: nil)

    The value of An SMTP Reply Code

Returns:

  • (String)

    The preferred value

Since:

  • v5.0.0



793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
# File 'lib/sisimai/smtp/status.rb', line 793

def prefer(argv0 = nil, argv1 = nil, argv2 = nil)
  return argv1 if argv0.nil?; return argv1 if argv0.size < 5
  return argv0 if argv1.nil?; return argv0 if argv1.size < 5

  statuscode = argv0
  codeinmesg = argv1; return codeinmesg if statuscode.index(".9.") == 1
  esmtpreply = argv2 || '000'
  the1stchar = {
    'field' => statuscode[0, 1].to_i,
    'error' => codeinmesg[0, 1].to_i,
    'reply' => esmtpreply.to_s[0, 1].to_i,
  }

  if the1stchar['reply'] > 0 && the1stchar['field'] != the1stchar['error']
    # There is the 3rd argument (an SMTP Reply Code)
    # Returns the value of $argv0 or $argv1 which begins with the 1st character of argv2
    return statuscode if the1stchar['reply'] == the1stchar['field']
    return codeinmesg if the1stchar['reply'] == the1stchar['error']
  end
  return statuscode if statuscode == codeinmesg

  zeroindex1 = {'field' => statuscode.index('.0')   || -1, 'error' => codeinmesg.index('.0')   || -1}
  zeroindex2 = {'field' => statuscode.index('.0.0') || -1, 'error' => codeinmesg.index('.0.0') || -1}

  if zeroindex2['field'] > 0
    # "Status:" field is "X.0.0"
    return codeinmesg if zeroindex2['error'] < 0
    return statuscode
  end

  if zeroindex1['field'] > 0
    # "Status:" field is "X.Y.0" or "X.0.Z"
    return codeinmesg if zeroindex1['error'] < 0
  end

  return statuscode if zeroindex2['error'] > 0        # An SMTP status code is "X.0.0"
  return codeinmesg if statuscode.start_with?('5.3.') # "5.3.Z" is an error of a system
  return codeinmesg if statuscode.end_with?('.5.1', '.5.2', '.5.4', '.5.5')

  case statuscode
    # - "4.4.7" is an ambigous code
    # - "4.7.0" indicates "too many errors"
    # - "X.5.1" indicates an invalid command
    # - "X.5.2" indicates a syntax error
    # - "X.5.4" indicates an invalid command argument
    # - "X.5.5" indicates a wrong protocol version
  when "4.4.7", "4.7.0" then return codeinmesg
  when "5.1.3"          then return codeinmesg if codeinmesg.start_with?('5.7.')
  when "5.1.1"
    # "5.1.1" is a code of "userunknown"
    return statuscode if codeinmesg.start_with?('5.5.') || zeroindex1['error'] > 0
    return codeinmesg
  end

  return statuscode
end

.test(argv1 = '') ⇒ Boolean

Check whether a status code is a valid code or not

Parameters:

  • argv1 (String) (defaults to: '')

    Status code(DSN)

Returns:

  • (Boolean)

    0 = Invalid status code, 1 = Valid status code

See Also:

Since:

  • v5.0.0



670
671
672
673
674
675
676
677
678
679
680
# File 'lib/sisimai/smtp/status.rb', line 670

def test(argv1 = '')
  return false if argv1.to_s.empty? || argv1.size < 5 || argv1.size > 7

  token = []
  argv1.split('.').each { |e| token << e.to_i }
  return false if token.size != 3
  return false if token[0] < 2 || token[0] == 3 || token[0]  > 5
  return false if token[1] < 0 || token[1]  > 7
  return false if token[2] < 0
  return true
end