Module: Sisimai::ARF
- Defined in:
- lib/sisimai/arf.rb
Overview
Sisimai::ARF is a decoder for the email returned as a FeedBack Loop report message.
Constant Summary collapse
- Indicators =
tools.ietf.org/html/rfc5965 en.wikipedia.org/wiki/Feedback_loop_(email) en.wikipedia.org/wiki/Abuse_Reporting_Format
Netease DMARC uses: This is a spf/dkim authentication-failure report for an email message received from IP OpenDMARC 1.3.0 uses: This is an authentication failure report for an email message received from IP Abusix ARF uses this is an autogenerated email abuse complaint regarding your network.
Sisimai::Lhost.INDICATORS
- AbuseAddrs =
['staff@hotmail.com', 'complaints@email-abuse.amazonses.com'].freeze
- ReportFrom =
"Content-Type: message/feedback-report".freeze
- Boundaries =
[ "Content-Type: message/rfc822", "Content-Type: text/rfc822-headers", "Content-Type: text/rfc822-header", # ?? ].freeze
- ARFPreface =
[ ["this is a", "abuse report"], ["this is a", "authentication", "failure report"], ["this is a", " report for"], ["this is an authentication", "failure report"], ["this is an autogenerated email abuse complaint"], ["this is an email abuse report"], ].freeze
Class Method Summary collapse
- .description ⇒ Object
- .inquire(mhead, mbody) ⇒ Hash, Nil abstract
-
.is_arf(heads) ⇒ True, False
Email is a Feedback-Loop message or not.
Class Method Details
.description ⇒ Object
32 |
# File 'lib/sisimai/arf.rb', line 32 def description; return 'Abuse Feedback Reporting Format'; end |
.inquire(mhead, mbody) ⇒ Hash, Nil
This method is abstract.
Detect an error for Feedback Loop
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 |
# File 'lib/sisimai/arf.rb', line 67 def inquire(mhead, mbody) return nil if self.is_arf(mhead) == false dscontents = [Sisimai::Lhost.DELIVERYSTATUS]; v = dscontents[-1] emailparts = Sisimai::RFC5322.part(mbody, Boundaries) bodyslices = emailparts[0].split("\n") reportpart = false readcursor = 0 # Points the current cursor position recipients = 0 # The number of 'Final-Recipient' header = "" # The value of "Arrival-Date" or "Received-Date" remotehost = "" # The value of "Source-IP" field reportedby = "" # The value of "Reporting-MTA" field anotherone = "" # Other fields(append to Diagnosis) # 3.1. Required Fields # # The following report header fields MUST appear exactly once: # # o "Feedback-Type" contains the type of feedback report (as defined # in the corresponding IANA registry and later in this memo). This # is intended to let report parsers distinguish among different # types of reports. # # o "User-Agent" indicates the name and version of the software # program that generated the report. The format of this field MUST # follow section 14.43 of [HTTP]. This field is for documentation # only; there is no registry of user agent names or versions, and # report receivers SHOULD NOT expect user agent names to belong to a # known set. # # o "Version" indicates the version of specification that the report # generator is using to generate the report. The version number in # this specification is set to "1". # while e = bodyslices.shift do # This is an email abuse report for an email message with the # message-id of 0000-000000000000000000000000000000000@mx # received from IP address 192.0.2.1 on # Thu, 29 Apr 2010 00:00:00 +0900 (JST) if readcursor == 0 # Beginning of the bounce message or message/delivery-status part r = e.downcase ARFPreface.each do |f| # Hello, # this is an autogenerated email abuse complaint regarding your network. next if Sisimai::String.aligned(r, f) == false readcursor |= Indicators[:deliverystatus] v["diagnosis"] += " #{e}" break end next end next if (readcursor & Indicators[:deliverystatus]) > 0 || e.empty? if e == ReportFrom then reportpart = true; next; end if reportpart # Feedback-Type: abuse # User-Agent: SomeGenerator/1.0 # Version: 0.1 # Original-Mail-From: <somespammer@example.net> # Original-Rcpt-To: <kijitora@example.jp> # Received-Date: Thu, 29 Apr 2009 00:00:00 JST # Source-IP: 192.0.2.1 if e.start_with?("Original-Rcpt-To: ") || e.start_with?("Removal-Recipient: ") # Original-Rcpt-To header field is optional and may appear any number of times as appropriate: # Original-Rcpt-To: <kijitora@example.jp> # Removal-Recipient: user@example.com cv = Sisimai::Address.s3s4(e[e.index(" ") + 1, e.size]); 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 elsif e.start_with?("Feedback-Type: ") # The header field MUST appear exactly once. # Feedback-Type: abuse v["feedbacktype"] = e[e.index(" ") + 1, e.size] elsif e.start_with?("Authentication-Results: ") # "Authentication-Results" indicates the result of one or more authentication checks # run by the report generator. # # Authentication-Results: mail.example.com; # spf=fail smtp.mail=somespammer@example.com anotherone += "#{e}, " elsif e.start_with?("User-Agent: ") # The header field MUST appear exactly once. # User-Agent: SomeGenerator/1.0 anotherone += "#{e}, " elsif e.start_with?("Received-Date: ") || e.start_with?("Arrival-Date: ") # Arrival-Date header is optional and MUST NOT appear more than once. # Received-Date: Thu, 29 Apr 2010 00:00:00 JST # Arrival-Date: Thu, 29 Apr 2010 00:00:00 +0000 = e[e.index(" ") + 1, e.size] elsif e.start_with?("Reporting-MTA: ") # The header is optional and MUST NOT appear more than once. # Reporting-MTA: dns; mx.example.jp cv = Sisimai::RFC1894.field(e); next if cv.size == 0 reportedby = cv[2] elsif e.start_with?("Source-IP: ") # The header is optional and MUST NOT appear more than once. # Source-IP: 192.0.2.45 remotehost = e[e.index(" ") + 1, e.size] elsif e.start_with?("Original-Mail-From: ") # the header is optional and MUST NOT appear more than once. # Original-Mail-From: <somespammer@example.net> anotherone += "#{e}, " end else # Messages before "Content-Type: message/feedback-report" part v["diagnosis"] += " #{e}" end while recipients == 0 # There is no recipient address in the message if mhead.has_key?("x-apple-unsubscribe") # X-Apple-Unsubscribe: true last if mhead["x-apple-unsubscribe"] != "true" || mhead["from"].include?('@') == false dscontents[0]["recipient"] = mhead["from"] dscontents[0]["diagnosis"] = emailparts[0] dscontents[0]["feedbacktype"] = "opt-out" # Addpend To: field as a pseudo header emailparts[1] = sprintf("To: <%s>\n", mhead["from"]) if emailparts[1].empty? else # Pick it from the original message part p1 = emailparts[1].index("\nTo:"); break if p1.nil? p2 = emailparts[1].index("\n", p1 + 4); break if p2.nil? cv = Sisimai::Address.s3s4(emailparts[1][p1 + 4, p2 - p1]) # There is no valid email address in the To: header of the original message such as # To: <Undisclosed Recipients> cv = Sisimai::Address.undisclosed("r") if Sisimai::Address.is_emailaddress(cv) == false dscontents[0]["recipient"] = cv end recipients += 1 end return nil if recipients == 0 anotherone = ": #{anotherone.chop}" if anotherone != "" j = -1 dscontents.each do |e| # Tidy up the error message in e.Diagnosis, Try to detect the bounce reason. j += 1 e["diagnosis"] = e["diagnosis"] + anotherone e["reason"] = "feedback" e["rhost"] = remotehost e["lhost"] = reportedby e["date"] = # Copy some values from the previous element when the report have 2 or more email address next if j == 0 || dscontents.size == 1 e["diagnosis"] = dscontents[j - 1]["diagnosis"] e["feedbacktype"] = dscontents[j - 1]["feedbacktype"] end end return { "ds" => dscontents, "rfc822" => emailparts[1] } end |
.is_arf(heads) ⇒ True, False
Email is a Feedback-Loop message or not
38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
# File 'lib/sisimai/arf.rb', line 38 def is_arf(heads) return false if heads.nil? # Content-Type: multipart/report; report-type=feedback-report; ... return true if Sisimai::String.aligned(heads["content-type"], ["report-type=", "feedback-report"]) if heads["content-type"].include?("multipart/mixed") # Microsoft (Hotmail, MSN, Live, Outlook) uses its own report format. # Amazon SES Complaints bounces cv = Sisimai::Address.s3s4(heads['from']) if heads["subject"].include?("complaint about message from ") # From: staff@hotmail.com # From: complaints@email-abuse.amazonses.com # Subject: complaint about message from 192.0.2.1 return true if AbuseAddrs.any? { |a| cv.include?(a) } end end # X-Apple-Unsubscribe: true return false if heads.has_key?("x-apple-unsubscribe") == false return true if heads["x-apple-unsubscribe"] == "true" return false end |