module Sisimai::Lhost::V5sendmail

Sisimai::Lhost::V5sendmail parses a bounce email which created by Sendmail version 5. Methods in the module are called from only Sisimai::Message.

Constants

Indicators
MarkingsOf
ReBackbone
StartingOf

Public Class Methods

description() click to toggle source
# File lib/sisimai/lhost/v5sendmail.rb, line 146
def description; return 'Sendmail version 5'; end
make(mhead, mbody) click to toggle source

Parse bounce messages from Sendmail version 5 @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/v5sendmail.rb, line 38
def make(mhead, mbody)
  # :from => %r/\AMail Delivery Subsystem/,
  return nil unless mhead['subject'] =~ /\AReturned mail: [A-Z]/

  emailsteak = Sisimai::RFC5322.fillet(mbody, ReBackbone)
  return nil if emailsteak[1].empty?

  dscontents = [Sisimai::Lhost.DELIVERYSTATUS]
  bodyslices = emailsteak[0].split("\n")
  readcursor = 0      # (Integer) Points the current cursor position
  recipients = 0      # (Integer) The number of 'Final-Recipient' header
  responding = []     # (Array) Responses from remote server
  commandset = []     # (Array) SMTP command which is sent to remote server
  anotherset = {}     # (Hash) Another error information
  errorindex = -1     # (Integer)
  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.include?(StartingOf[:message][0])
      next
    end
    next if (readcursor & Indicators[:deliverystatus]) == 0
    next if e.empty?

    #    ----- Transcript of session follows -----
    # While talking to smtp.example.com:
    # >>> RCPT To:<kijitora@example.org>
    # <<< 550 <kijitora@example.org>, User Unknown
    # 550 <kijitora@example.org>... User unknown
    # 421 example.org (smtp)... Deferred: Connection timed out during user open with example.org
    v = dscontents[-1]

    if cv = e.match(/\A\d{3}[ ]+[<]([^ ]+[@][^ ]+)[>][.]{3}[ ]*(.+)\z/)
      # 550 <kijitora@example.org>... User unknown
      if v['recipient']
        # There are multiple recipient addresses in the message body.
        dscontents << Sisimai::Lhost.DELIVERYSTATUS
        v = dscontents[-1]
      end
      v['recipient'] = cv[1]
      v['diagnosis'] = cv[2]

      # Concatenate the response of the server and error message
      v['diagnosis'] << ': ' << responding[recipients] if responding[recipients]
      recipients += 1

    elsif cv = e.match(/\A[>]{3}[ ]*([A-Z]{4})[ ]*/)
      # >>> RCPT To:<kijitora@example.org>
      commandset[recipients] = cv[1]

    elsif cv = e.match(/\A[<]{3}[ ]+(.+)\z/)
      # <<< Response
      # <<< 501 <shironeko@example.co.jp>... no access from mail server [192.0.2.55] which is an open relay.
      # <<< 550 Requested User Mailbox not found. No such user here.
      responding[recipients] = cv[1]
    else
      # Detect SMTP session error or connection error
      next if v['sessionerr']

      if e =~ MarkingsOf[:error]
        # ----- Transcript of session follows -----
        # ... while talking to mta.example.org.:
        v['sessionerr'] = true
        next
      end

      if cv = e.match(/\A\d{3}[ ]+.+[.]{3}[ \t]*(.+)\z/)
        # 421 example.org (smtp)... Deferred: Connection timed out during user open with example.org
        anotherset['diagnosis'] = cv[1]
      end
    end
  end

  if recipients == 0 && cv = emailsteak[1].match(/^To:[ ]*(.+)$/)
    # Get the recipient address from "To:" header at the original message
    dscontents[0]['recipient'] = Sisimai::Address.s3s4(cv[1])
    recipients = 1
  end
  return nil unless recipients > 0

  dscontents.each do |e|
    errorindex += 1
    e['command'] = commandset[errorindex] || ''
    e['diagnosis'] ||= if anotherset['diagnosis'].to_s.size > 0
                         # Copy alternative error message
                         anotherset['diagnosis']
                       else
                         # Set server response as a error message
                         responding[errorindex]
                       end
    e['diagnosis'] = Sisimai::String.sweep(e['diagnosis'])

    unless e['recipient'] =~ /\A[^ ]+[@][^ ]+\z/
      # @example.jp, no local part
      if cv = e['diagnosis'].match(/[<]([^ ]+[@][^ ]+)[>]/)
        # Get email address from the value of Diagnostic-Code header
        e['recipient'] = cv[1]
      end
    end
    e.delete('sessionerr')
  end

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