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
.description ⇒ Object
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
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'] = {} # (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 = 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 [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 { = 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 && 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. .each_key { |a| e[a] ||= [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 |