Module: Sisimai::RFC3464

Defined in:
lib/sisimai/rfc3464.rb,
lib/sisimai/rfc3464/thirdparty.rb

Overview

Sisimai::RFC3464 - bounce mail decoder class for Fallback.

Defined Under Namespace

Modules: ThirdParty

Constant Summary collapse

Indicators =
Sisimai::Lhost.INDICATORS
Boundaries =
[
  # When the new value added, the part of the value should be listed in delimiters variable
  # defined at Sisimai::RFC2045.makeFlat() method
  "Content-Type: message/rfc822",
  "Content-Type: text/rfc822-headers",
  "Content-Type: message/partial",
  "Content-Disposition: inline", # See lhost-amavis-*.eml, lhost-facebook-*.eml
].freeze
StartingOf =
{message: ["Content-Type: message/delivery-status"]}.freeze
FieldTable =
Sisimai::RFC1894.FIELDTABLE

Class Method Summary collapse

Class Method Details

.descriptionObject



271
# File 'lib/sisimai/rfc3464.rb', line 271

def description; 'RFC3464'; end

.inquire(mhead, mbody) ⇒ Hash, Nil

Decode a bounce mail which have fields defined in RFC3464

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
# File 'lib/sisimai/rfc3464.rb', line 31

def inquire(mhead, mbody)
  # There is no "Content-Type: message/rfc822" line in the message body
  if Boundaries.any? { |a| mbody.include?(a) } == false
    # Insert "Content-Type: message/rfc822" before "Return-Path:" of the original message
    p0 = mbody.index("\n\nReturn-Path:")
    mbody = sprintf("%s%s%s", mbody[0, p0], Boundaries[0], mbody[p0 + 1, mbody.size]) if p0
  end

  permessage = {}
  dscontents = [Sisimai::Lhost.DELIVERYSTATUS]; v = nil
  alternates = Sisimai::Lhost.DELIVERYSTATUS
  emailparts = Sisimai::RFC5322.part(mbody, Boundaries)
  readslices = [""]
  readcursor = 0      # (Integer) Points the current cursor position
  recipients = 0      # (Integer) The number of 'Final-Recipient' header
  beforemesg = ""     # (String) String before StartingOf[:message]
  goestonext = false  # (Bool) Flag: do not append the line into beforemesg
  isboundary = [Sisimai::RFC2045.boundary(mhead["content-type"], 0)]; isboundary[0] ||= ""

  while emailparts[0].index('@').nil? do
    # There is no email address in the first element of emailparts
    # There is a bounce message inside of message/rfc822 part at lhost-x5-*
    p0 = -1 # The index of the boundary string found first
    p1 =  0 # Offset position of the message body after the boundary string
    ct = "" # Boundary string found first such as "Content-Type: message/rfc822"

    Boundaries.each do |e|
      # Look for a boundary string from the message body
      p0 = mbody.index(e + "\n"); next if p0.nil?
      p1 = p0 + e.size + 2
      ct = e; break
    end
    break if p0.nil?

    cx = mbody[p1, mbody.size]
    p2 = cx.index("\n\n")
    cv = cx[p2 + 2, mbody.size]
    emailparts = Sisimai::RFC5322.part(cv, [ct], 0)
    break
  end

  if emailparts[0].index(StartingOf[:message][0]) == nil
    # There is no "Content-Type: message/delivery-status" line in the message body
    # Insert "Content-Type: message/delivery-status" before "Reporting-MTA:" field
    cv = "\nReporting-MTA:"
    e0 = emailparts[0]
    p0 = e0.index(cv)
    emailparts[0] = sprintf("%s\n\n%s%s", e0[0, p0], StartingOf[:message][0], e0[p0, e0.size]) if p0
  end

  %w[Final-Recipient Original-Recipient].each do |e|
    # Fix the malformed field "Final-Recipient: <kijitora@example.jp>"
    cv = "\n" + e + ": "
    cx = cv + "<"
    p0 = emailparts[0].index(cx); next if p0.nil?

    emailparts[0] = emailparts[0].sub(": <", ": rfc822; ")
    p1 = emailparts[0].index(">\n", p0 + 2); emailparts[0][p1, 1] = ""
  end

  bodyslices = emailparts[0].scrub('?').split("\n")
  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 delivery status part
      readcursor |= Indicators[:deliverystatus] if e.start_with?(StartingOf[:message][0])

      while true do
        # Append each string before startingof["message"][0] except the following patterns
        # for the later reference
        break if e.empty?   # Blank line
        break if goestonext # Skip if the part is text/html, image/icon, in multipart/*

        # This line is a boundary kept in "multiparts" as a string, when the end of the boundary
        # appeared, the condition above also returns true.
        if isboundary.any? { |a| e == a } then goestonext = false; break; end
        if e.start_with?("Content-Type:")
          # Content-Type: field in multipart/*
          case
            # Content-Type: multipart/alternative; boundary=aa00220022222222ffeebb
            # Pick the boundary string and store it into "isboucdary"
            when e.include?("multipart/") then isboundary << Sisimai::RFC2045.boundary(e, 0)
            when e.include?("text/plain") then goestonext = false
            else goestonext = true # Other types: for example, text/html, image/jpg, and so on
          end
          break
        end

        break if e.start_with?("Content-")        # Content-Disposition, ...
        break if e.start_with?("This is a MIME")  # This is a MIME-formatted message.
        break if e.start_with?("This is a multi") # This is a multipart message in MIME format
        break if e.start_with?("This is an auto") # This is an automatically generated ...
        break if e.start_with?("This multi-part") # This multi-part MIME message contains...
        break if e.start_with?("###")             # A frame like #####
        break if e.start_with?("***")             # A frame like *****
        break if e.start_with?("--")              # Boundary string
        break if e.include?("--- The follow")     # ----- The following addresses had delivery problems -----
        break if e.include?("--- Transcript")     # ----- Transcript of session follows -----
        beforemesg += "#{e} "; break
      end
      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 o[0] == "final-recipient"
          # Final-Recipient: rfc822; kijitora@example.jp
          # Final-Recipient: x400; /PN=...
          cv = Sisimai::Address.s3s4(o[2]); next if Sisimai::Address.is_emailaddress(cv) == false
          cw = dscontents.size;             next if cw > 0 && cv == dscontents[cw - 1]["recipient"]

          if v["recipient"] != ""
            # There are multiple recipient addresses in the message body.
            dscontents << Sisimai::Lhost.DELIVERYSTATUS
            v = dscontents[-1]
          end
          v["recipient"] = cv
          recipients += 1
        else
          # X-Actual-Recipient: rfc822; kijitora@example.co.jp
          v["alias"] = o[2]
        end
      when "code"
        # Diagnostic-Code: SMTP; 550 5.1.1 <userunknown@example.jp>... User Unknown
        v["spec"]      = o[1]
        v["diagnosis"] = o[2]
      else
        # Other DSN fields defined in RFC3464
        if o[4].size > 0
          # There are other error messages as a comment such as the following:
          # Status: 5.0.0 (permanent failure)
          # Status: 4.0.0 (cat.example.net: host name lookup failure)
          v["diagnosis"] += " #{o[4]} "
        end
        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
      # Check that the line is a continued line of the value of Diagnostic-Code: field or not
      if e.start_with?("X-") && e.include?(": ")
        # This line is a MTA-Specific fields begins with "X-"
        next if Sisimai::RFC3464::ThirdParty.is3rdparty(e) == false
        cv = Sisimai::RFC3464::ThirdParty.xfield(e)

        if cv.size > 0 && FieldTable[cv[0].downcase] == nil
          # Check the first element is a field defined in RFC1894 or not
          p1 = cv[4].index(":")
          v["reason"] = cv[4][p1 + 1, cv[4].size] if cv[4].start_with?("reason:")
        else
          # Set the value picked from "X-*" field to $dscontents when the current value is empty
          z = FieldTable[cv[0].downcase]; next if z.nil?
          v[z] ||=  cv[2]
        end
      else
        # The line may be a continued line of the value of the Diagnostic-Code: field
        if readslices[-2].start_with?("Diagnostic-Code:") == false
          # In the case of multiple "message/delivery-status" line
          next if e.start_with?("Content-") # Content-Disposition:, ...
          next if e.start_with?("--")       # Boundary string
          beforemesg += "#{e} "; next
        end

        # Diagnostic-Code: SMTP; 550-5.7.26 The MAIL FROM domain [email.example.jp]
        #    has an SPF record with a hard fail
        next if e.start_with?(" ") == false
        v["diagnosis"] += " " + e
      end
    end
  end

  while recipients == 0 do
    # There is no valid recipient address, Try to use the alias addaress as a final recipient
    break if dscontents[0]["alias"].nil? || dscontents[0]["alias"].empty?
    break if Sisimai::Address.is_emailaddress(dscontents[0]["alias"]) == false
    dscontents[0]["recipient"] = dscontents[0]["alias"]
    recipients += 1
  end
  return nil if recipients == 0

  require "sisimai/smtp/reply"
  require "sisimai/smtp/status"
  require "sisimai/smtp/command"

  if beforemesg != ""
    # Pick some values of $dscontents from the string before StartingOf[:message]
    beforemesg = beforemesg.split.join(" ")
    alternates["command"]   = Sisimai::SMTP::Command.find(beforemesg)
    alternates["replycode"] = Sisimai::SMTP::Reply.find(beforemesg, dscontents[0]["status"])
    alternates["status"]    = Sisimai::SMTP::Status.find(beforemesg, alternates["replycode"])
  end
  issuedcode = beforemesg.downcase

  dscontents.each do |e|
    # Set default values stored in "permessage" if each value in "dscontents" is empty.
    permessage.each_key { |a| e[a] ||= permessage[a] || '' }
    lowercased = e["diagnosis"].downcase

    if recipients == 1
      # Do not mix the error message of each recipient with "beforemesg" when there is
      # multiple recipient addresses in the bounce message
      if issuedcode.include?(lowercased)
        # beforemesg contains the entire strings of e["diagnosis"]
        e["diagnosis"] = beforemesg
      else
        # The value of e["diagnosis"] is not contained in $beforemesg
        # There may be an important error message in $beforemesg
        e["diagnosis"] = sprintf("%s %s", beforemesg, e["diagnosis"])
      end
    end
    e["command"]   = Sisimai::SMTP::Command.find(e["diagnosis"])
    e["command"]   = alternates["command"]   if e["command"].empty? 
    e["replycode"] = Sisimai::SMTP::Reply.find(e["diagnosis"], e["status"])
    e["replycode"] = alternates["replycode"] if e["replycode"].empty? 
    e["status"]    = Sisimai::SMTP::Status.find(e["diagnosis"], e["replycode"]) if e["status"].empty?
    e["status"]    = alternates["status"] if e["status"].empty?
  end

  if emailparts[1].nil? || emailparts[1].empty?
    # Set the recipient address as To: header in the original message part
    emailparts[1] = sprintf("To: <%s>\n", dscontents[0]["recipient"])
  end
  return { "ds" => dscontents, "rfc822" => emailparts[1] }
end