Module: Sisimai::Lhost::Postfix

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

Overview

Sisimai::Lhost::Postfix decodes a bounce email which created by Postfix www.postfix.org/. Methods in the module are called from only Sisimai::Message.

Constant Summary collapse

Indicators =

Postfix manual - bounce(5) - www.postfix.org/bounce.5.html

Sisimai::Lhost.INDICATORS
Boundaries =
['Content-Type: message/rfc822', 'Content-Type: text/rfc822-headers'].freeze
StartingOf =
{
  # Postfix manual - bounce(5) - http://www.postfix.org/bounce.5.html
  message: [
    ['The ', 'Postfix '],           # The Postfix program, The Postfix on <os> program
    ['The ', 'mail system'],        # The mail system
    ['The ', 'program'],            # The <name> pogram
    ['This is the', 'Postfix'],     # This is the Postfix program
    ['This is the', 'mail system'], # This is the mail system at host <hostname>
  ],
}.freeze

Class Method Summary collapse

Class Method Details

.descriptionObject



287
# File 'lib/sisimai/lhost/postfix.rb', line 287

def description; return 'Postfix'; end

.inquire(mhead, mbody) ⇒ Hash, Nil

This method is abstract.

Decodes the bounce message from Postfix

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



31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
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
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
# File 'lib/sisimai/lhost/postfix.rb', line 31

def inquire(mhead, mbody)
  match = 0

  if mhead['subject'].include?('SMTP server: errors from ')
    # src/smtpd/smtpd_chat.c:|337: post_mail_fprintf(notice, "Subject: %s SMTP server: errors from %s",
    # src/smtpd/smtpd_chat.c:|338:   var_mail_name, state->namaddr);
    match = 2
  else
    # Subject: Undelivered Mail Returned to Sender
    match = 1 if mhead['subject'] == 'Undelivered Mail Returned to Sender'
  end
  return nil if match == 0 || mhead['x-aol-ip']

  permessage = {}     # (Hash) Store values of each Per-Message field
  dscontents = [Sisimai::Lhost.DELIVERYSTATUS]; v = nil
  emailparts = Sisimai::RFC5322.part(mbody, Boundaries)
  bodyslices = emailparts[0].split("\n")
  readslices = ['']
  recipients = 0      # (Integer) The number of 'Final-Recipient' header
  nomessages = false  # (Boolean) Delivery report unavailable
  commandset = []     # (Array) ``in reply to * command'' list
  anotherset = {}     # Another error information

  if match == 2
    # The message body starts with 'Transcript of session follows.'
    require 'sisimai/smtp/transcript'
    transcript = Sisimai::SMTP::Transcript.rise(emailparts[0], 'In:', 'Out:')

    return nil if transcript.nil? || transcript.size == 0

    transcript.each do |e|
      # Pick email addresses, error messages, and the last SMTP command.
      v ||= dscontents[-1]
      p   = e['response']

      case e["command"]
      # Use the argument of EHLO/HELO command as a value of "lhost"
      when "HELO", "EHLO" then v['lhost'] = e['argument']
      when "MAIL"
        # Set the argument of "MAIL" command to pseudo To: header of the original message
        emailparts[1] += sprintf("To: %s\n", e['argument']) if emailparts[1].size == 0

      when "RCPT"
        # RCPT TO: <...>
        if v["recipient"] != ""
          # There are multiple recipient addresses in the transcript of session
          dscontents << Sisimai::Lhost.DELIVERYSTATUS
          v = dscontents[-1]
        end
        v['recipient'] = e['argument']
        recipients += 1
      end

      next if p['reply'].to_i < 400
      commandset << e['command']
      v['diagnosis'] = p['text'].join(' ') if v["diagnosis"].empty?
      v['replycode'] = p['reply'] if v["replycode"].empty?
      v['status']    = p['status'] if v["status"].empty?
    end
  else
    fieldtable = Sisimai::RFC1894.FIELDTABLE
    readcursor = 0      # (Integer) Points the current cursor position

    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.
      readslices << e # Save the current line for the next loop

      if readcursor == 0
        # Beginning of the bounce message or message/delivery-status part
        readcursor |= Indicators[:deliverystatus] if StartingOf[:message].any? { |a| Sisimai::String.aligned(e, a) }
        next
      end
      next if (readcursor & Indicators[:deliverystatus]) == 0 || e.empty?

      f = Sisimai::RFC1894.match(e)
      if f > 0
        # "e" matched with any field defined in RFC3464
        next unless o = Sisimai::RFC1894.field(e)
        v = dscontents[-1]

        case o[3]
        when "addr"
          # Final-Recipient: rfc822; kijitora@example.jp
          # X-Actual-Recipient: rfc822; kijitora@example.co.jp
          if Sisimai::Address.is_emailaddress(o[2])
            # The email address is a valid email address, avoid an email address
            # without a valid domain part such as "neko@mailhost".
            if o[0] == 'final-recipient'
              # Final-Recipient: rfc822; 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'] = o[2]
              recipients += 1
            else
              # X-Actual-Recipient: rfc822; kijitora@example.co.jp
              v['alias'] = o[2]
            end
          end
        when "code"
          # Diagnostic-Code: SMTP; 550 5.1.1 <userunknown@example.jp>... User Unknown
          v['spec'] = o[1]
          v['spec'] = 'SMTP' if v['spec'].upcase == 'X-POSTFIX'
          v['diagnosis'] = o[2]
        else
          # Other DSN fields defined in RFC3464
          next if fieldtable[o[0]].nil?
          next if o[3] == "host" && Sisimai::RFC1123.is_internethost(o[2]) == false
          v[fieldtable[o[0]]] = o[2]

          next if f != 1
          permessage[fieldtable[o[0]]] = o[2]
        end
      else
        # If you do so, please include this problem report. You can
        # delete your own text from the attached returned message.
        #
        #           The mail system
        #
        # <userunknown@example.co.jp>: host mx.example.co.jp[192.0.2.153] said: 550
        # 5.1.1 <userunknown@example.co.jp>... User Unknown (in reply to RCPT TO command)
        if readslices[-2].start_with?('Diagnostic-Code:') && e.include?(' ')
          # Continued line of the value of Diagnostic-Code header
          v['diagnosis'] += " " + e.split.join(" ")
          readslices[-1]  = "Diagnostic-Code: #{e}"

        elsif Sisimai::String.aligned(e, ['X-Postfix-Sender:', 'rfc822;', '@'])
          # X-Postfix-Sender: rfc822; shironeko@example.org
          emailparts[1] += "X-Postfix-Sender: #{Sisimai::Address.s3s4(e[e.index(';') + 1, e.size])}\n"

        else
          # Alternative error message and recipient
          if e.include?(' (in reply to ') || e.include?('command)')
            # 5.1.1 <userunknown@example.co.jp>... User Unknown (in reply to RCPT TO
            cv = Sisimai::SMTP::Command.find(e) || ""; commandset << cv if cv.empty? == false
            anotherset['diagnosis'] ||= ''
            anotherset['diagnosis']  += " #{e}"

          elsif Sisimai::String.aligned(e, ['<', '@', '>', '(expanded from ', '):'])
            # <r@example.ne.jp> (expanded from <kijitora@example.org>): user ...
            p1 = e.index('> ');                   next unless p1
            p2 = e.index('(expanded from ', p1);  next unless p2
            p3 = e.index('>): ', p2 + 14);        next unless p3
            anotherset['recipient'] = Sisimai::Address.s3s4(e[0, p1])
            anotherset['alias']     = Sisimai::Address.s3s4(e[p2 + 15, p3 - p2 - 15])
            anotherset['diagnosis'] = e[p3 + 3, e.size]

          elsif e.start_with?('<') && Sisimai::String.aligned(e, ['<', '@', '>:'])
            # <kijitora@exmaple.jp>: ...
            anotherset['recipient'] = Sisimai::Address.s3s4(e[0, e.index('>')])
            anotherset['diagnosis'] = e[e.index('>:') + 2, e.size]

          elsif e.include?('--- Delivery report unavailable ---')
            # postfix-3.1.4/src/bounce/bounce_notify_util.c
            # bounce_notify_util.c:602|if (bounce_info->log_handle == 0
            # bounce_notify_util.c:602||| bounce_log_rewind(bounce_info->log_handle)) {
            # bounce_notify_util.c:602|if (IS_FAILURE_TEMPLATE(bounce_info->template)) {
            # bounce_notify_util.c:602|    post_mail_fputs(bounce, "");
            # bounce_notify_util.c:602|    post_mail_fputs(bounce, "\t--- delivery report unavailable ---");
            # bounce_notify_util.c:602|    count = 1;              /* xxx don't abort */
            # bounce_notify_util.c:602|}
            # bounce_notify_util.c:602|} else {
            nomessages = true
          else
            # Get an error message continued from the previous line
            next if anotherset['diagnosis'].nil?
            if e.start_with?('    ')
              #    host mx.example.jp said:...
              anotherset['diagnosis'] += " #{e[4, e.size]}"
            end
          end
        end
      end
    end # end of while()
  end

  if recipients == 0
    # Fallback: get a recipient address from error messages
    %w[recipient alias].each do |e|
      # Set a valid recipient address picked from the anotherset
      next  if anotherset[e].nil? || Sisimai::Address.is_emailaddress(anotherset[e]) == false
      break if dscontents[-1]['recipient'].empty? == false
      dscontents[-1]['recipient'] = anotherset[e]
      recipients += 1
      break
    end

    if recipients == 0
      # Get a recipient address from message/rfc822 part if the delivery report was unavailable:
      # '--- Delivery report unavailable ---'
      p1 = emailparts[1].index("\nTo: ")     || -1
      p2 = emailparts[1].index("\n", p1 + 6) || -1
      if nomessages && p1 > 0
        # Try to get a recipient address from To: field in the original message at message/rfc822 part
        dscontents[-1]['recipient'] = Sisimai::Address.s3s4(emailparts[1][p1 + 5, p2 - p1 - 5])
        recipients += 1
      end
    end
  end
  return nil if recipients == 0

  dscontents.each do |e|
    # Set default values if each value is empty.
    permessage.each_key { |a| e[a] ||= permessage[a] || '' }

    if anotherset['diagnosis']
      # Copy alternative error message
      anotherset['diagnosis'] = anotherset['diagnosis'].split.join(" ")
      e['diagnosis'] = anotherset['diagnosis'] if e['diagnosis'].nil? || e['diagnosis'].empty?

      if e['diagnosis'] =~ /\A\d+\z/
        # Override the value of diagnostic code message
        e['diagnosis'] = anotherset['diagnosis']
      else
        # More detailed error message is in "anotherset"
        as = '' # status
        ar = '' # replycode
        if Sisimai::SMTP::Status.is_ambiguous(e['status'])
          # Check the value of D.S.N. in anotherset
          # The D.S.N. is neither an empty nor *.0.0
          as = Sisimai::SMTP::Status.find(anotherset['diagnosis'])
          e['status'] = as if Sisimai::SMTP::Status.is_ambiguous(as) == false
        end

        if e['replycode'].empty? || e['replycode'].end_with?('00')
          # Check the value of SMTP reply code in $anotherset
          ar = Sisimai::SMTP::Reply.find(anotherset['diagnosis'])
          if ar.size > 0 && ar.end_with?('00') == false
            # The SMTP reply code is neither an empty nor *00
            e['replycode'] = ar
          end
        end

        while true
          # Replace e['diagnosis'] with the value of anotherset['diagnosis'] when all the
          # following conditions have not matched.
          break if (as + ar).size == 0
          break if anotherset['diagnosis'].size < e['diagnosis'].size
          break if anotherset['diagnosis'].include?(e['diagnosis']) == false

          e['diagnosis'] = anotherset['diagnosis']
          break
        end
      end
    end

    e['command']   = commandset.shift || Sisimai::SMTP::Command.find(e['diagnosis'])
    e['command']   = 'HELO' if e["command"].empty? && e['diagnosis'].include?('refused to talk to me:')
    e['spec']      = 'SMTP' if e["spec"].empty?    && Sisimai::String.aligned(e['diagnosis'], ['host ', ' said:'])
  end

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