module Sisimai::Lhost::Qmail

Sisimai::Lhost::Qmail parses a bounce email which created by qmail. Methods in the module are called from only Sisimai::Message.

Constants

FailOnLDAP
HasExpired

qmail-send.c:922| … (&dline,“I'm not going to try again; this message has been in the queue too long.n”)) nomem();

Indicators
MessagesOf
ReBackbone
ReCommands
ReHost

qmail-remote.c:261| if (!flagbother) quit(“DGiving up on ”,“”);

ReIsOnHold
ReSMTP
StartingOf

Public Class Methods

description() click to toggle source
# File lib/sisimai/lhost/qmail.rb, line 235
def description; return 'qmail'; end
make(mhead, mbody) click to toggle source

Parse bounce messages from qmail @param [Hash] mhead Message headers of a bounce email @param [String] mbody Message body of a bounce email @return [Hash] Bounce data list and message/rfc822 part @return [Nil] it failed to parse or the arguments are missing

# File lib/sisimai/lhost/qmail.rb, line 107
def make(mhead, mbody)
  # Pre process email headers and the body part of the message which generated
  # by qmail, see https://cr.yp.to/qmail.html
  #   e.g.) Received: (qmail 12345 invoked for bounce); 29 Apr 2009 12:34:56 -0000
  #         Subject: failure notice
  tryto  = /\A[(]qmail[ ]+\d+[ ]+invoked[ ]+(?:for[ ]+bounce|from[ ]+network)[)]/
  match  = 0
  match += 1 if mhead['subject'] == 'failure notice'
  match += 1 if mhead['received'].any? { |a| a =~ tryto }
  return nil unless match > 0

  dscontents = [Sisimai::Lhost.DELIVERYSTATUS]
  emailsteak = Sisimai::RFC5322.fillet(mbody, ReBackbone)
  bodyslices = emailsteak[0].split("\n")
  readcursor = 0      # (Integer) Points the current cursor position
  recipients = 0      # (Integer) The number of 'Final-Recipient' header
  v = nil

  while e = bodyslices.shift do
    # Read error messages and delivery status lines from the head of the email
    # to the previous line of the beginning of the original message.
    if readcursor == 0
      # Beginning of the bounce message or delivery status part
      readcursor |= Indicators[:deliverystatus] if e.start_with?(StartingOf[:message][0])
      next
    end
    next if (readcursor & Indicators[:deliverystatus]) == 0
    next if e.empty?

    # <kijitora@example.jp>:
    # 192.0.2.153 does not like recipient.
    # Remote host said: 550 5.1.1 <kijitora@example.jp>... User Unknown
    # Giving up on 192.0.2.153.
    v = dscontents[-1]

    if cv = e.match(/\A(?:To[ ]*:)?[<](.+[@].+)[>]:[ \t]*\z/)
      # <kijitora@example.jp>:
      if v['recipient']
        # There are multiple recipient addresses in the message body.
        dscontents << Sisimai::Lhost.DELIVERYSTATUS
        v = dscontents[-1]
      end
      v['recipient'] = cv[1]
      recipients += 1

    elsif dscontents.size == recipients
      # Append error message
      next if e.empty?
      v['diagnosis'] ||= ''
      v['diagnosis'] << e + ' '
      v['alterrors'] = e if e.start_with?(StartingOf[:error][0])

      next if v['rhost']
      next unless cv = e.match(ReHost)
      v['rhost'] = cv[1]
    end
  end
  return nil unless recipients > 0

  dscontents.each do |e|
    e['agent']     = 'qmail'
    e['diagnosis'] = Sisimai::String.sweep(e['diagnosis']) || ''

    unless e['command']
      # Get the SMTP command name for the session
      ReSMTP.each_key do |r|
        # Verify each regular expression of SMTP commands
        next unless e['diagnosis'] =~ ReSMTP[r]
        e['command'] = r.upcase
        break
      end

      unless e['command']
        # Verify each regular expression of patches
        if cv = e['diagnosis'].match(ReCommands) then e['command'] = cv[1].upcase end
        e['command'] ||= ''
      end
    end

    # Detect the reason of bounce
    if e['command'] == 'MAIL'
      # MAIL | Connected to 192.0.2.135 but sender was rejected.
      e['reason'] = 'rejected'

    elsif %w[HELO EHLO].index(e['command'])
      # HELO | Connected to 192.0.2.135 but my name was rejected.
      e['reason'] = 'blocked'
    else
      # Try to match with each error message in the table
      if e['diagnosis'] =~ ReIsOnHold
        # To decide the reason require pattern match with
        # Sisimai::Reason::* modules
        e['reason'] = 'onhold'
      else
        MessagesOf.each_key do |r|
          # Verify each regular expression of session errors
          if e['alterrors']
            # Check the value of "alterrors"
            next unless MessagesOf[r].any? { |a| e['alterrors'].include?(a) }
            e['reason'] = r
          end
          break if e['reason']

          next unless MessagesOf[r].any? { |a| e['diagnosis'].include?(a) }
          e['reason'] = r
          break
        end

        unless e['reason']
          FailOnLDAP.each_key do |r|
            # Verify each regular expression of LDAP errors
            next unless FailOnLDAP[r].any? { |a| e['diagnosis'].include?(a) }
            e['reason'] = r
            break
          end
        end

        unless e['reason']
          e['reason'] = 'expired' if e['diagnosis'].include?(HasExpired)
        end
      end
    end

    e['status'] = Sisimai::SMTP::Status.find(e['diagnosis']) || ''
  end

  return { 'ds' => dscontents, 'rfc822' => emailsteak[1] }
end