@@ -32,17 +32,23 @@ type ListedErr struct {
3232 Identity string
3333 List string
3434 Reason string
35+ Score int
36+ Message string
3537}
3638
3739func (le ListedErr ) Fields () map [string ]interface {} {
40+ msg := "Client identity listed in the used DNSBL"
41+ if le .Message != "" {
42+ msg = le .Message
43+ }
3844 return map [string ]interface {}{
3945 "check" : "dnsbl" ,
4046 "list" : le .List ,
4147 "listed_identity" : le .Identity ,
4248 "reason" : le .Reason ,
4349 "smtp_code" : 554 ,
4450 "smtp_enchcode" : exterrors.EnhancedCode {5 , 7 , 0 },
45- "smtp_msg" : "Client identity listed in the used DNSBL" ,
51+ "smtp_msg" : msg ,
4652 }
4753}
4854
@@ -66,28 +72,85 @@ func checkDomain(ctx context.Context, resolver dns.Resolver, cfg List, domain st
6672 return nil
6773 }
6874
69- // Attempt to extract explanation string.
70- txts , err := resolver .LookupTXT (context .Background (), query )
71- if err != nil || len (txts ) == 0 {
72- // Not significant, include addresses as reason. Usually they are
73- // mapped to some predefined 'reasons' by BL.
74- return ListedErr {
75- Identity : domain ,
76- List : cfg .Zone ,
77- Reason : strings .Join (addrs , "; " ),
75+ var score int
76+ var customMessage string
77+ var filteredAddrs []string
78+
79+ // If ResponseRules is configured, use new behavior
80+ if len (cfg .ResponseRules ) > 0 {
81+ // Convert string addresses to IPAddr for matching
82+ ipAddrs := make ([]net.IPAddr , 0 , len (addrs ))
83+ for _ , addr := range addrs {
84+ if ip := net .ParseIP (addr ); ip != nil {
85+ ipAddrs = append (ipAddrs , net.IPAddr {IP : ip })
86+ }
7887 }
88+
89+ matchedScore , matchedMessages , matchedReasons , matched := matchResponseRules (ipAddrs , cfg .ResponseRules )
90+ if ! matched {
91+ return nil
92+ }
93+ score = matchedScore
94+
95+ // Use first matched message if available
96+ if len (matchedMessages ) > 0 {
97+ customMessage = matchedMessages [0 ]
98+ }
99+
100+ filteredAddrs = matchedReasons
101+ } else {
102+ // Legacy behavior: accept all addresses
103+ filteredAddrs = addrs
79104 }
80105
81- // Some BLs provide multiple reasons (meta-BLs such as Spamhaus Zen) so
82- // don't mangle them by joining with "", instead join with "; ".
106+ // Attempt to extract explanation string from TXT records (shared by both paths)
107+ txts , err := resolver .LookupTXT (ctx , query )
108+ var reason string
109+ if err == nil && len (txts ) > 0 {
110+ reason = strings .Join (txts , "; " )
111+ } else {
112+ // Not significant, include addresses as reason. Usually they are
113+ // mapped to some predefined 'reasons' by BL.
114+ reason = strings .Join (filteredAddrs , "; " )
115+ }
83116
84117 return ListedErr {
85118 Identity : domain ,
86119 List : cfg .Zone ,
87- Reason : strings .Join (txts , "; " ),
120+ Reason : reason ,
121+ Score : score ,
122+ Message : customMessage ,
88123 }
89124}
90125
126+ func matchResponseRules (addrs []net.IPAddr , rules []ResponseRule ) (score int , messages []string , reasons []string , matched bool ) {
127+ // Track which rules have been matched to avoid counting the same rule multiple times
128+ matchedRules := make (map [int ]bool )
129+
130+ for _ , addr := range addrs {
131+ for ruleIdx , rule := range rules {
132+ // Skip if this rule has already been matched
133+ if matchedRules [ruleIdx ] {
134+ continue
135+ }
136+
137+ for _ , respNet := range rule .Networks {
138+ if respNet .Contains (addr .IP ) {
139+ score += rule .Score
140+ if rule .Message != "" {
141+ messages = append (messages , rule .Message )
142+ }
143+ reasons = append (reasons , addr .IP .String ())
144+ matchedRules [ruleIdx ] = true
145+ matched = true
146+ break // Move to next rule
147+ }
148+ }
149+ }
150+ }
151+ return
152+ }
153+
91154func checkIP (ctx context.Context , resolver dns.Resolver , cfg List , ip net.IP ) error {
92155 ipv6 := true
93156 if ipv4 := ip .To4 (); ipv4 != nil {
@@ -113,52 +176,72 @@ func checkIP(ctx context.Context, resolver dns.Resolver, cfg List, ip net.IP) er
113176 return err
114177 }
115178
116- filteredAddrs := make ([]net.IPAddr , 0 , len (addrs ))
117- addrsLoop:
118- for _ , addr := range addrs {
119- // No responses whitelist configured - permit all.
120- if len (cfg .Responses ) == 0 {
121- filteredAddrs = append (filteredAddrs , addr )
122- continue
179+ var filteredAddrs []net.IPAddr
180+ var score int
181+ var customMessage string
182+
183+ // If ResponseRules is configured, use new behavior
184+ if len (cfg .ResponseRules ) > 0 {
185+ matchedScore , matchedMessages , matchedReasons , matched := matchResponseRules (addrs , cfg .ResponseRules )
186+ if ! matched {
187+ return nil
123188 }
189+ score = matchedScore
124190
125- for _ , respNet := range cfg .Responses {
126- if respNet .Contains (addr .IP ) {
191+ // Use first matched message if available
192+ if len (matchedMessages ) > 0 {
193+ customMessage = matchedMessages [0 ]
194+ }
195+
196+ // Build filteredAddrs from matched reasons for TXT lookup fallback
197+ for _ , reason := range matchedReasons {
198+ filteredAddrs = append (filteredAddrs , net.IPAddr {IP : net .ParseIP (reason )})
199+ }
200+ } else {
201+ // Legacy behavior: use flat Responses filter
202+ filteredAddrs = make ([]net.IPAddr , 0 , len (addrs ))
203+ addrsLoop:
204+ for _ , addr := range addrs {
205+ // No responses whitelist configured - permit all.
206+ if len (cfg .Responses ) == 0 {
127207 filteredAddrs = append (filteredAddrs , addr )
128- continue addrsLoop
208+ continue
209+ }
210+
211+ for _ , respNet := range cfg .Responses {
212+ if respNet .Contains (addr .IP ) {
213+ filteredAddrs = append (filteredAddrs , addr )
214+ continue addrsLoop
215+ }
129216 }
130217 }
131- }
132218
133- if len (filteredAddrs ) == 0 {
134- return nil
219+ if len (filteredAddrs ) == 0 {
220+ return nil
221+ }
135222 }
136223
137- // Attempt to extract explanation string.
224+ // Attempt to extract explanation string from TXT records (shared by both paths)
138225 txts , err := resolver .LookupTXT (ctx , query )
139- if err != nil || len (txts ) == 0 {
226+ var reason string
227+ if err == nil && len (txts ) > 0 {
228+ reason = strings .Join (txts , "; " )
229+ } else {
140230 // Not significant, include addresses as reason. Usually they are
141231 // mapped to some predefined 'reasons' by BL.
142-
143232 reasonParts := make ([]string , 0 , len (filteredAddrs ))
144233 for _ , addr := range filteredAddrs {
145234 reasonParts = append (reasonParts , addr .IP .String ())
146235 }
147-
148- return ListedErr {
149- Identity : ip .String (),
150- List : cfg .Zone ,
151- Reason : strings .Join (reasonParts , "; " ),
152- }
236+ reason = strings .Join (reasonParts , "; " )
153237 }
154238
155- // Some BLs provide multiple reasons (meta-BLs such as Spamhaus Zen) so
156- // don't mangle them by joining with "", instead join with "; ".
157-
158239 return ListedErr {
159240 Identity : ip .String (),
160241 List : cfg .Zone ,
161- Reason : strings .Join (txts , "; " ),
242+ Reason : reason ,
243+ Score : score ,
244+ Message : customMessage ,
162245 }
163246}
164247
0 commit comments