Module: Sisimai::Lhost::Qmail

Defined in:
lib/sisimai/lhost/qmail.rb

Overview

Sisimai::Lhost::Qmail decodes a bounce email which created by qmail cr.yp.to/qmail.html or qmail clones or notqmail notqmail.org/. Methods in the module are called from only Sisimai::Message.

Constant Summary collapse

Indicators =
Sisimai::Lhost.INDICATORS
Boundaries =
[
  # qmail-send.c:qmail_puts(&qqt,*sender.s ? "--- Below this line is a copy of the message.\n\n" :...
  "--- Below this line is a copy of the message.",     # qmail-1.03
  "--- Below this line is a copy of the mail header.",
  "--- Below the next line is a copy of the message.", # The followings are the qmail clone
  "--- Mensaje original adjunto.",
  "Content-Type: message/rfc822",
  "Original message follows.",
].freeze
EmailTitle =
[
  "failure notice", # qmail-send.c:Subject: failure notice\n\
  "Failure Notice", # Yahoo
].freeze
StartingOf =
{
  #  qmail-remote.c:248|    if (code >= 500) {
  #  qmail-remote.c:249|      out("h"); outhost(); out(" does not like recipient.\n");
  #  qmail-remote.c:265|  if (code >= 500) quit("D"," failed on DATA command");
  #  qmail-remote.c:271|  if (code >= 500) quit("D"," failed after I sent the message");
  #
  # Characters: K,Z,D in qmail-qmqpc.c, qmail-send.c, qmail-rspawn.c
  #  K = success, Z = temporary error, D = permanent error
  "error"   => ["Remote host said:"],
  "message" => [
    "Hi. This is the qmail", # qmail-send.c:Hi. This is the qmail-send program at ");
    "He/Her is not ",        # The followings are the qmail clone
    "unable to deliver your message to the following addresses",
    "Su mensaje no pudo ser entregado",
    "Sorry, we were unable to deliver your message to the following address",
    "This is the machine generated message from mail service",
    "This is the mail delivery agent at",
    "Unable to deliver message to the following address",
    "unable to deliver your message to the following addresses",
    "Unfortunately, your mail was not delivered to the following address:",
    "Your mail message to the following address",
    "Your message to the following addresses",
    "We're sorry.",
  ],
  "rhost"   => ['Giving up on ', 'Connected to ', 'remote host '],
}.freeze
CommandSet =
{
  # qmail-remote.c:225|  if (smtpcode() != 220) quit("ZConnected to "," but greeting failed");
  "CONN" => [" but greeting failed."],
  # qmail-remote.c:231|  if (smtpcode() != 250) quit("ZConnected to "," but my name was rejected");
  "EHLO" => [" but my name was rejected."],
  # qmail-remote.c:238|  if (code >= 500) quit("DConnected to "," but sender was rejected");
  # reason = rejected
  "MAIL" => [" but sender was rejected."],
  # qmail-remote.c:249|  out("h"); outhost(); out(" does not like recipient.\n");
  # qmail-remote.c:253|  out("s"); outhost(); out(" does not like recipient.\n");
  # reason = userunknown
  "RCPT" => [" does not like recipient."],
  # qmail-remote.c:265|  if (code >= 500) quit("D"," failed on DATA command");
  # qmail-remote.c:266|  if (code >= 400) quit("Z"," failed on DATA command");
  # qmail-remote.c:271|  if (code >= 500) quit("D"," failed after I sent the message");
  # qmail-remote.c:272|  if (code >= 400) quit("Z"," failed after I sent the message");
  "DATA" => [" failed on DATA command", " failed after I sent the message"],
}.freeze
MessagesOf =
{
  # notqmail 1.08 returns the following error message when the destination MX is NullMX
  "notaccept"   => ["Sorry, I couldn't find a mail exchanger or IP address"],
  "userunknown" => ["no mailbox here by that name"],
}.freeze

Class Method Summary collapse

Class Method Details

.descriptionObject



186
# File 'lib/sisimai/lhost/qmail.rb', line 186

def description; return 'qmail'; end

.inquire(mhead, mbody) ⇒ Hash, Nil

This method is abstract.

Decodes the bounce message from qmail

Parameters:

  • mhead (Hash)

    Message headers of a bounce email

  • mbody (String)

    Message body of a bounce email

Returns:

  • (Hash)

    Bounce data list and message/rfc822 part

  • (Nil)

    it failed to decode or the arguments are missing



80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
# File 'lib/sisimai/lhost/qmail.rb', line 80

def inquire(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
  proceedsto = false
  proceedsto = true if EmailTitle.any? { |a| mhead["subject"] == a }
  mhead["received"].each do |e|
    # Received: (qmail 2222 invoked for bounce);29 Apr 2017 23:34:45 +0900
    # Received: (qmail 2202 invoked from network); 29 Apr 2018 00:00:00 +0900
    proceedsto = true if Sisimai::String.aligned(e, ["(qmail", " invoked "])
  end
  return nil if proceedsto == false

  require "sisimai/smtp/command"
  dscontents = [Sisimai::Lhost.DELIVERYSTATUS]; v = nil
  emailparts = Sisimai::RFC5322.part(mbody, Boundaries)
  bodyslices = emailparts[0].split("\n")
  readcursor = 0      # (Integer) Points the current cursor position
  recipients = 0      # (Integer) The number of 'Final-Recipient' header

  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 StartingOf["message"].any? { |a| e.include?(a) }
      next
    end
    next if (readcursor & Indicators[:deliverystatus]) == 0 || 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 e.start_with?('<') && Sisimai::String.aligned(e, ['<', '@', '>:'])
      # <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"] = Sisimai::Address.s3s4(e[e.index("<"), e.size])
      recipients += 1

    elsif dscontents.size == recipients
      # Append error message
      v["diagnosis"] += "#{e} "
      v["alterrors"]  = e if e.start_with?(StartingOf["error"][0])

      next if v["rhost"] != ""
      StartingOf["rhost"].each do |r|
        # Find a remote host name
        p1 = e.index(r); next if p1.nil?
        cm = r.size
        p2 = e.index(" ", p1 + cm + 1) || p2 = e.rindex(".") 

        v["rhost"] = e[p1 + cm, p2 - p1 - cm]
        break
      end
    end
  end
  return nil if recipients == 0

  dscontents.each do |e|
    # Get the SMTP command name for the session
    CommandSet.each_key do |r|
      # Get the last SMTP command
      next if CommandSet[r].none? { |a| e["diagnosis"].include?(a) }
      e["command"] = r
      break
    end

    if e["diagnosis"].include?("Sorry, no SMTP connection got far enough")
      # Sorry, no SMTP connection got far enough; most progress was RCPT TO response; ...
      e["command"] = Sisimai::SMTP::Command.find(e["diagnosis"]) if e["command"].empty?
    end

    # Detect the reason of bounce
    if %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
      # Check that the error message includes any of message patterns or not
      [e["alterrors"], e["diagnosis"]].each do |f|
        # Try to detect an error reason
        break if e["reason"] != ""
        next  if f.nil?
        MessagesOf.each_key do |r|
          # The key is a bounce reason name
          next if MessagesOf[r].none? { |a| f.include?(a) }
          e["reason"] = r
          break
        end
        break if e["reason"]
      end
    end

    e["command"] = Sisimai::SMTP::Command.find(e["diagnosis"]) if e["command"].empty?
  end

  return { "ds" => dscontents, "rfc822" => emailparts[1] }
end