module Sisimai::Lhost::Exim
Sisimai::Lhost::Exim
parses a bounce email which created by Exim
. Methods in the module are called from only Sisimai::Message
.
Constants
- DelayedFor
retry.c:902| addr->message = (addr->message == NULL)? US“retry timeout exceeded” : deliver.c:7475| “No action is required on your part. Delivery attempts will continue forn” smtp.c:3508| US“retry time not reached for any host after a long failure period” : smtp.c:3508| US“all hosts have been failing for a long time and were last tried ”
"after this message arrived";
deliver.c:7459| print_address_error(addr, f, US“Delay reason: ”); deliver.c:7586| “Message %s has been frozen%s.nThe sender is <%s>.n”, message_id, receive.c:4021| moan_tell_someone(freeze_tell, NULL, US“Message frozen on arrival”, receive.c:4022| “Message %s was frozen on arrival by %s.nThe sender is <%s>.n”,
- Indicators
- MarkingsOf
- MessagesOf
- ReBackbone
deliver.c:6423| if (bounce_return_body) fprintf(f, deliver.c:6424|“—— This is a copy of the message, including all the headers. ——n”); deliver.c:6425| else fprintf(f, deliver.c:6426|“—— This is a copy of the message's headers. ——n”);
- ReCommands
- StartingOf
Public Class Methods
# File lib/sisimai/lhost/exim.rb, line 490 def description; return 'Exim'; end
Parse bounce messages from Exim
@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/exim.rb, line 128 def make(mhead, mbody) return nil if mhead['from'] =~ /[@].+[.]mail[.]ru[>]?/ # Message-Id: <E1P1YNN-0003AD-Ga@example.org> # X-Failed-Recipients: kijitora@example.ed.jp match = 0 match += 1 if mhead['from'].start_with?('Mail Delivery System') match += 1 if mhead['message-id'].to_s =~ %r/\A[<]\w{7}[-]\w{6}[-]\w{2}[@]/ match += 1 if mhead['subject'] =~ %r{(?: Mail[ ]delivery[ ]failed(:[ ]returning[ ]message[ ]to[ ]sender)? |Warning:[ ]message[ ][^ ]+[ ]delayed[ ]+ |Delivery[ ]Status[ ]Notification |Mail[ ]failure |Message[ ]frozen |error[(]s[)][ ]in[ ]forwarding[ ]or[ ]filtering ) }x return nil if match < 2 require 'sisimai/rfc1894' fieldtable = Sisimai::RFC1894.FIELDTABLE dscontents = [Sisimai::Lhost.DELIVERYSTATUS] emailsteak = Sisimai::RFC5322.fillet(mbody, ReBackbone) bodyslices = emailsteak[0].split("\n") readcursor = 0 # (Integer) Points the current cursor position nextcursor = 0 recipients = 0 # (Integer) The number of 'Final-Recipient' header localhost0 = '' # (String) Local MTA boundary00 = '' # (String) Boundary string v = nil if mhead['content-type'] # Get the boundary string and set regular expression for matching with # the boundary string. boundary00 = Sisimai::MIME.boundary(mhead['content-type']) || '' end 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 message/delivery-status part if e =~ MarkingsOf[:message] readcursor |= Indicators[:deliverystatus] next unless e =~ MarkingsOf[:frozen] end end next if (readcursor & Indicators[:deliverystatus]) == 0 next if e.empty? # This message was created automatically by mail delivery software. # # A message that you sent could not be delivered to one or more of its # recipients. This is a permanent error. The following address(es) failed: # # kijitora@example.jp # SMTP error from remote mail server after RCPT TO:<kijitora@example.jp>: # host neko.example.jp [192.0.2.222]: 550 5.1.1 <kijitora@example.jp>... User Unknown v = dscontents[-1] if cv = e.match(/\A[ \t]{2}([^ \t]+[@][^ \t]+[.]?[a-zA-Z]+)(:.+)?\z/) || e.match(/\A[ \t]{2}[^ \t]+[@][^ \t]+[.][a-zA-Z]+[ ]<(.+?[@].+?)>:.+\z/) || e.match(MarkingsOf[:alias]) # kijitora@example.jp # sabineko@example.jp: forced freeze # mikeneko@example.jp <nekochan@example.org>: ... # # deliver.c:4549| printed = US"an undisclosed address"; # an undisclosed address # (generated from kijitora@example.jp) r = cv[1] if v['recipient'] # There are multiple recipient addresses in the message body. dscontents << Sisimai::Lhost.DELIVERYSTATUS v = dscontents[-1] end # v['recipient'] = cv[1] if cv = e.match(/\A[ \t]+[^ \t]+[@][^ \t]+[.][a-zA-Z]+[ ]<(.+?[@].+?)>:.+\z/) # parser.c:743| while (bracket_count-- > 0) if (*s++ != '>') # parser.c:744| { # parser.c:745| *errorptr = s[-1] == 0 # parser.c:746| ? US"'>' missing at end of address" # parser.c:747| : string_sprintf("malformed address: %.32s may not follow %.*s", # parser.c:748| s-1, (int)(s - US mailbox - 1), mailbox); # parser.c:749| goto PARSE_FAILED; # parser.c:750| } r = cv[1] v['diagnosis'] = e end v['recipient'] = r recipients += 1 elsif cv = e.match(/\A[ ]+[(]generated[ ]from[ ](.+)[)]\z/) || e.match(/\A[ ]+generated[ ]by[ ]([^ \t]+[@][^ \t]+)/) # (generated from kijitora@example.jp) # pipe to |/bin/echo "Some pipe output" # generated by userx@myhost.test.ex v['alias'] = cv[1] else next if e.empty? if e =~ MarkingsOf[:frozen] # Message *** has been frozen by the system filter. # Message *** was frozen on arrival by ACL. v['alterrors'] ||= '' v['alterrors'] << e + ' ' else if !boundary00.empty? # --NNNNNNNNNN-eximdsn-MMMMMMMMMM # Content-type: message/delivery-status # ... if Sisimai::RFC1894.match(e) # "e" matched with any field defined in RFC3464 next unless o = Sisimai::RFC1894.field(e) if o[-1] == 'addr' # Final-Recipient: rfc822; kijitora@example.jp # X-Actual-Recipient: rfc822; kijitora@example.co.jp next unless o[0] == 'final-recipient' v['spec'] ||= o[2].include?('@') ? 'SMTP' : 'X-UNIX' elsif o[-1] == '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 next unless fieldtable[o[0]] v[fieldtable[o[0]]] = o[2] end else # Error message ? next if nextcursor == 1 # Content-type: message/delivery-status nextcursor = 1 if e.start_with?(StartingOf[:deliverystatus][0]) v['alterrors'] ||= '' if e.start_with?("\s", "\t") e.sub!(/\A[\s\t]+/, '') v['alterrors'] << e + ' ' unless v['alterrors'].include?(e) end end else if dscontents.size == recipients # Error message next if e.empty? v['diagnosis'] ||= '' v['diagnosis'] << e + ' ' else # Error message when email address above does not include '@' # and domain part. if e =~ %r<\A[ ]+pipe[ ]to[ ][|]/[^ ]+> # pipe to |/path/to/prog ... # generated by kijitora@example.com v['diagnosis'] = e else next unless e.start_with?(' ') v['alterrors'] ||= '' v['alterrors'] << e + ' ' end end end end end end if recipients > 0 # Check "an undisclosed address", "unroutable address" dscontents.each do |q| # Replace the recipient address with the value of "alias" next unless q['alias'] next if q['alias'].empty? if q['recipient'].empty? || q['recipient'].include?('@') == false # The value of "recipient" is empty or does not include "@" q['recipient'] = q['alias'] end end else # Fallback for getting recipient addresses if mhead['x-failed-recipients'] # X-Failed-Recipients: kijitora@example.jp rcptinhead = mhead['x-failed-recipients'].split(',') rcptinhead.each do |a| # Remove space characters a.lstrip! a.rstrip! end recipients = rcptinhead.size while e = rcptinhead.shift do # Insert each recipient address into dscontents dscontents[-1]['recipient'] = e next if dscontents.size == recipients dscontents << Sisimai::Lhost.DELIVERYSTATUS end end end return nil unless recipients > 0 unless mhead['received'].empty? # Get the name of local MTA # Received: from marutamachi.example.org (c192128.example.net [192.0.2.128]) if cv = mhead['received'][-1].match(/from[ \t]([^ ]+)/) then localhost0 = cv[1] end end dscontents.each do |e| # Set default values if each value is empty. e['lhost'] ||= localhost0 unless e['diagnosis'] # Empty Diagnostic-Code: or error message unless boundary00.empty? # --NNNNNNNNNN-eximdsn-MMMMMMMMMM # Content-type: message/delivery-status # # Reporting-MTA: dns; the.local.host.name # # Action: failed # Final-Recipient: rfc822;/a/b/c # Status: 5.0.0 # # Action: failed # Final-Recipient: rfc822;|/p/q/r # Status: 5.0.0 e['diagnosis'] = dscontents[0]['diagnosis'] || '' e['spec'] ||= dscontents[0]['spec'] unless dscontents[0]['alterrors'].to_s.empty? # The value of "alterrors" is also copied e['alterrors'] = dscontents[0]['alterrors'] end end end unless e['alterrors'].to_s.empty? # Copy alternative error message if e['diagnosis'].nil? || e['diagnosis'].empty? e['diagnosis'] = e['alterrors'] end if e['diagnosis'].start_with?('-') || e['diagnosis'].end_with?('__') # Override the value of diagnostic code message e['diagnosis'] = e['alterrors'] unless e['alterrors'].empty? else # Check the both value and try to match if e['diagnosis'].size < e['alterrors'].size # Check the value of alterrors rxdiagnosis = %r/e['diagnosis']/i # Override the value of diagnostic code message because # the value of alterrors includes the value of diagnosis. e['diagnosis'] = e['alterrors'] if e['alterrors'].downcase.include?(e['diagnosis'].downcase) end end e.delete('alterrors') end e['diagnosis'] = Sisimai::String.sweep(e['diagnosis']) || '' e['diagnosis'].sub!(/\b__.+\z/, '') unless e['rhost'] # Get the remote host name # host neko.example.jp [192.0.2.222]: 550 5.1.1 <kijitora@example.jp>... User Unknown if cv = e['diagnosis'].match(/host[ \t]+([^ \t]+)[ \t]\[.+\]:[ \t]/) then e['rhost'] = cv[1] end unless e['rhost'] # Get localhost and remote host name from Received header. e['rhost'] = Sisimai::RFC5322.received(mhead['received'][-1]).pop unless mhead['received'].empty? end end unless e['command'] # Get the SMTP command name for the session ReCommands.each do |r| # Verify each regular expression of SMTP commands if cv = e['diagnosis'].match(r) e['command'] = cv[1].upcase break end 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' elsif e['command'] == 'MAIL' # MAIL | Connected to 192.0.2.135 but sender was rejected. e['reason'] = 'onhold' else # Verify each regular expression of session errors MessagesOf.each_key do |r| # Check each regular expression next unless MessagesOf[r].any? { |a| e['diagnosis'].include?(a) } e['reason'] = r break end unless e['reason'] # The reason "expired" e['reason'] = 'expired' if DelayedFor.any? { |a| e['diagnosis'].include?(a) } end end end # Prefer the value of smtp reply code in Diagnostic-Code: # See eg/maildir-as-a-sample/new/exim-20.eml # Action: failed # Final-Recipient: rfc822;userx@test.ex # Status: 5.0.0 # Remote-MTA: dns; 127.0.0.1 # Diagnostic-Code: smtp; 450 TEMPERROR: retry timeout exceeded # The value of "Status:" indicates permanent error but the value # of SMTP reply code in Diagnostic-Code: field is "TEMPERROR"!!!! sv = Sisimai::SMTP::Status.find(e['diagnosis']) rv = Sisimai::SMTP::Reply.find(e['diagnosis']) s1 = 0 # First character of Status as integer r1 = 0 # First character of SMTP reply code as integer while true # "Status:" field did not exist in the bounce message break if sv break unless rv # Check SMTP reply code # Generate pseudo DSN code from SMTP reply code r1 = rv[0, 1].to_i if r1 == 4 # Get the internal DSN(temporary error) sv = Sisimai::SMTP::Status.code(e['reason'], true) elsif r1 == 5 # Get the internal DSN(permanent error) sv = Sisimai::SMTP::Status.code(e['reason'], false) end break end s1 = sv[0, 1].to_i if sv v1 = s1 + r1 v1 << e['status'][0, 1].to_i if e['status'] if v1 > 0 # Status or SMTP reply code exists # Set pseudo DSN into the value of "status" accessor e['status'] = sv if r1 > 0 else # Neither Status nor SMTP reply code exist sv = if %w[expired mailboxfull].include?(e['reason']) # Set pseudo DSN (temporary error) Sisimai::SMTP::Status.code(e['reason'], true) else # Set pseudo DSN (permanent error) Sisimai::SMTP::Status.code(e['reason'], false) end end e['status'] ||= sv.to_s end return { 'ds' => dscontents, 'rfc822' => emailsteak[1] } end