PNG  IHDRQgAMA a cHRMz&u0`:pQ<bKGDgmIDATxwUﹻ& ^CX(J I@ "% (** BX +*i"]j(IH{~R)[~>h{}gy)I$Ij .I$I$ʊy@}x.: $I$Ii}VZPC)I$IF ^0ʐJ$I$Q^}{"r=OzI$gRZeC.IOvH eKX $IMpxsk.쒷/&r[޳<v| .I~)@$updYRa$I |M.e JaֶpSYR6j>h%IRز if&uJ)M$I vLi=H;7UJ,],X$I1AҒJ$ XY XzI@GNҥRT)E@;]K*Mw;#5_wOn~\ DC&$(A5 RRFkvIR}l!RytRl;~^ǷJj اy뷦BZJr&ӥ8Pjw~vnv X^(I;4R=P[3]J,]ȏ~:3?[ a&e)`e*P[4]T=Cq6R[ ~ޤrXR Հg(t_HZ-Hg M$ãmL5R uk*`%C-E6/%[t X.{8P9Z.vkXŐKjgKZHg(aK9ڦmKjѺm_ \#$5,)-  61eJ,5m| r'= &ڡd%-]J on Xm|{ RҞe $eڧY XYrԮ-a7RK6h>n$5AVڴi*ֆK)mѦtmr1p| q:흺,)Oi*ֺK)ܬ֦K-5r3>0ԔHjJئEZj,%re~/z%jVMڸmrt)3]J,T K֦OvԒgii*bKiNO~%PW0=dii2tJ9Jݕ{7"I P9JKTbu,%r"6RKU}Ij2HKZXJ,妝 XYrP ެ24c%i^IK|.H,%rb:XRl1X4Pe/`x&P8Pj28Mzsx2r\zRPz4J}yP[g=L) .Q[6RjWgp FIH*-`IMRaK9TXcq*I y[jE>cw%gLRԕiFCj-ďa`#e~I j,%r,)?[gp FI˨mnWX#>mʔ XA DZf9,nKҲzIZXJ,L#kiPz4JZF,I,`61%2s $,VOϚ2/UFJfy7K> X+6 STXIeJILzMfKm LRaK9%|4p9LwJI!`NsiazĔ)%- XMq>pk$-$Q2x#N ؎-QR}ᶦHZډ)J,l#i@yn3LN`;nڔ XuX5pF)m|^0(>BHF9(cզEerJI rg7 4I@z0\JIi䵙RR0s;$s6eJ,`n 䂦0a)S)A 1eJ,堌#635RIgpNHuTH_SԕqVe ` &S)>p;S$魁eKIuX`I4춒o}`m$1":PI<[v9^\pTJjriRŭ P{#{R2,`)e-`mgj~1ϣLKam7&U\j/3mJ,`F;M'䱀 .KR#)yhTq;pcK9(q!w?uRR,n.yw*UXj#\]ɱ(qv2=RqfB#iJmmL<]Y͙#$5 uTU7ӦXR+q,`I}qL'`6Kͷ6r,]0S$- [RKR3oiRE|nӦXR.(i:LDLTJjY%o:)6rxzҒqTJjh㞦I.$YR.ʼnGZ\ֿf:%55 I˼!6dKxm4E"mG_ s? .e*?LRfK9%q#uh$)i3ULRfK9yxm܌bj84$i1U^@Wbm4uJ,ҪA>_Ij?1v32[gLRD96oTaR׿N7%L2 NT,`)7&ƝL*꽙yp_$M2#AS,`)7$rkTA29_Iye"|/0t)$n XT2`YJ;6Jx".e<`$) PI$5V4]29SRI>~=@j]lp2`K9Jaai^" Ԋ29ORI%:XV5]JmN9]H;1UC39NI%Xe78t)a;Oi Ҙ>Xt"~G>_mn:%|~ޅ_+]$o)@ǀ{hgN;IK6G&rp)T2i୦KJuv*T=TOSV>(~D>dm,I*Ɛ:R#ۙNI%D>G.n$o;+#RR!.eU˽TRI28t)1LWϚ>IJa3oFbu&:tJ*(F7y0ZR ^p'Ii L24x| XRI%ۄ>S1]Jy[zL$adB7.eh4%%누>WETf+3IR:I3Xה)3אOۦSRO'ٺ)S}"qOr[B7ϙ.edG)^ETR"RtRݜh0}LFVӦDB^k_JDj\=LS(Iv─aTeZ%eUAM-0;~˃@i|l @S4y72>sX-vA}ϛBI!ݎߨWl*)3{'Y|iSlEڻ(5KtSI$Uv02,~ԩ~x;P4ցCrO%tyn425:KMlD ^4JRxSهF_}شJTS6uj+ﷸk$eZO%G*^V2u3EMj3k%)okI]dT)URKDS 7~m@TJR~荪fT"֛L \sM -0T KfJz+nإKr L&j()[E&I ߴ>e FW_kJR|!O:5/2跌3T-'|zX ryp0JS ~^F>-2< `*%ZFP)bSn"L :)+pʷf(pO3TMW$~>@~ū:TAIsV1}S2<%ޟM?@iT ,Eūoz%i~g|`wS(]oȤ8)$ ntu`өe`6yPl IzMI{ʣzʨ )IZ2= ld:5+請M$-ї;U>_gsY$ÁN5WzWfIZ)-yuXIfp~S*IZdt;t>KūKR|$#LcԀ+2\;kJ`]YǔM1B)UbG"IRߊ<xܾӔJ0Z='Y嵤 Leveg)$znV-º^3Ւof#0Tfk^Zs[*I꯳3{)ˬW4Ւ4 OdpbZRS|*I 55#"&-IvT&/윚Ye:i$ 9{LkuRe[I~_\ؠ%>GL$iY8 9ܕ"S`kS.IlC;Ҏ4x&>u_0JLr<J2(^$5L s=MgV ~,Iju> 7r2)^=G$1:3G< `J3~&IR% 6Tx/rIj3O< ʔ&#f_yXJiގNSz; Tx(i8%#4 ~AS+IjerIUrIj362v885+IjAhK__5X%nV%Iͳ-y|7XV2v4fzo_68"S/I-qbf; LkF)KSM$ Ms>K WNV}^`-큧32ŒVؙGdu,^^m%6~Nn&͓3ŒVZMsRpfEW%IwdǀLm[7W&bIRL@Q|)* i ImsIMmKmyV`i$G+R 0tV'!V)֏28vU7͒vHꦼtxꗞT ;S}7Mf+fIRHNZUkUx5SAJㄌ9MqμAIRi|j5)o*^'<$TwI1hEU^c_j?Е$%d`z cyf,XO IJnTgA UXRD }{H}^S,P5V2\Xx`pZ|Yk:$e ~ @nWL.j+ϝYb퇪bZ BVu)u/IJ_ 1[p.p60bC >|X91P:N\!5qUB}5a5ja `ubcVxYt1N0Zzl4]7­gKj]?4ϻ *[bg$)+À*x쳀ogO$~,5 زUS9 lq3+5mgw@np1sso Ӻ=|N6 /g(Wv7U;zωM=wk,0uTg_`_P`uz?2yI!b`kĸSo+Qx%!\οe|އԁKS-s6pu_(ֿ$i++T8=eY; צP+phxWQv*|p1. ά. XRkIQYP,drZ | B%wP|S5`~́@i޾ E;Չaw{o'Q?%iL{u D?N1BD!owPHReFZ* k_-~{E9b-~P`fE{AܶBJAFO wx6Rox5 K5=WwehS8 (JClJ~ p+Fi;ŗo+:bD#g(C"wA^ r.F8L;dzdIHUX݆ϞXg )IFqem%I4dj&ppT{'{HOx( Rk6^C٫O.)3:s(۳(Z?~ٻ89zmT"PLtw䥈5&b<8GZ-Y&K?e8,`I6e(֍xb83 `rzXj)F=l($Ij 2*(F?h(/9ik:I`m#p3MgLaKjc/U#n5S# m(^)=y=đx8ŬI[U]~SцA4p$-F i(R,7Cx;X=cI>{Km\ o(Tv2vx2qiiDJN,Ҏ!1f 5quBj1!8 rDFd(!WQl,gSkL1Bxg''՞^ǘ;pQ P(c_ IRujg(Wz bs#P­rz> k c&nB=q+ؔXn#r5)co*Ũ+G?7< |PQӣ'G`uOd>%Mctz# Ԫڞ&7CaQ~N'-P.W`Oedp03C!IZcIAMPUۀ5J<\u~+{9(FbbyAeBhOSܳ1 bÈT#ŠyDžs,`5}DC-`̞%r&ڙa87QWWp6e7 Rϫ/oY ꇅ Nܶըtc!LA T7V4Jsū I-0Pxz7QNF_iZgúWkG83 0eWr9 X]㾮݁#Jˢ C}0=3ݱtBi]_ &{{[/o[~ \q鯜00٩|cD3=4B_b RYb$óBRsf&lLX#M*C_L܄:gx)WΘsGSbuL rF$9';\4Ɍq'n[%p.Q`u hNb`eCQyQ|l_C>Lb꟟3hSb #xNxSs^ 88|Mz)}:](vbۢamŖ࿥ 0)Q7@0=?^k(*J}3ibkFn HjB׻NO z x}7p 0tfDX.lwgȔhԾŲ }6g E |LkLZteu+=q\Iv0쮑)QٵpH8/2?Σo>Jvppho~f>%bMM}\//":PTc(v9v!gոQ )UfVG+! 35{=x\2+ki,y$~A1iC6#)vC5^>+gǵ@1Hy٪7u;p psϰu/S <aʸGu'tD1ԝI<pg|6j'p:tպhX{o(7v],*}6a_ wXRk,O]Lܳ~Vo45rp"N5k;m{rZbΦ${#)`(Ŵg,;j%6j.pyYT?}-kBDc3qA`NWQū20/^AZW%NQ MI.X#P#,^Ebc&?XR tAV|Y.1!؅⨉ccww>ivl(JT~ u`ٵDm q)+Ri x/x8cyFO!/*!/&,7<.N,YDŽ&ܑQF1Bz)FPʛ?5d 6`kQձ λc؎%582Y&nD_$Je4>a?! ͨ|ȎWZSsv8 j(I&yj Jb5m?HWp=g}G3#|I,5v珿] H~R3@B[☉9Ox~oMy=J;xUVoj bUsl_35t-(ՃɼRB7U!qc+x4H_Qo֮$[GO<4`&č\GOc[.[*Af%mG/ ňM/r W/Nw~B1U3J?P&Y )`ѓZ1p]^l“W#)lWZilUQu`-m|xĐ,_ƪ|9i:_{*(3Gѧ}UoD+>m_?VPۅ15&}2|/pIOʵ> GZ9cmíتmnz)yߐbD >e}:) r|@R5qVSA10C%E_'^8cR7O;6[eKePGϦX7jb}OTGO^jn*媓7nGMC t,k31Rb (vyܴʭ!iTh8~ZYZp(qsRL ?b}cŨʊGO^!rPJO15MJ[c&~Z`"ѓޔH1C&^|Ш|rʼ,AwĴ?b5)tLU)F| &g٣O]oqSUjy(x<Ϳ3 .FSkoYg2 \_#wj{u'rQ>o;%n|F*O_L"e9umDds?.fuuQbIWz |4\0 sb;OvxOSs; G%T4gFRurj(֍ڑb uԖKDu1MK{1^ q; C=6\8FR艇!%\YÔU| 88m)֓NcLve C6z;o&X x59:q61Z(T7>C?gcļxѐ Z oo-08jہ x,`' ҔOcRlf~`jj".Nv+sM_]Zk g( UOPyεx%pUh2(@il0ݽQXxppx-NS( WO+轾 nFߢ3M<;z)FBZjciu/QoF 7R¥ ZFLF~#ȣߨ^<쩡ݛкvџ))ME>ώx4m#!-m!L;vv#~Y[đKmx9.[,UFS CVkZ +ߟrY٧IZd/ioi$%͝ب_ֶX3ܫhNU ZZgk=]=bbJS[wjU()*I =ώ:}-蹞lUj:1}MWm=̛ _ ¾,8{__m{_PVK^n3esw5ӫh#$-q=A̟> ,^I}P^J$qY~Q[ Xq9{#&T.^GVj__RKpn,b=`żY@^՝;z{paVKkQXj/)y TIc&F;FBG7wg ZZDG!x r_tƢ!}i/V=M/#nB8 XxЫ ^@CR<{䤭YCN)eKOSƟa $&g[i3.C6xrOc8TI;o hH6P&L{@q6[ Gzp^71j(l`J}]e6X☉#͕ ׈$AB1Vjh㭦IRsqFBjwQ_7Xk>y"N=MB0 ,C #o6MRc0|$)ف"1!ixY<B9mx `,tA>)5ػQ?jQ?cn>YZe Tisvh# GMމȇp:ԴVuږ8ɼH]C.5C!UV;F`mbBk LTMvPʍϤj?ԯ/Qr1NB`9s"s TYsz &9S%U԰> {<ؿSMxB|H\3@!U| k']$U+> |HHMLޢ?V9iD!-@x TIî%6Z*9X@HMW#?nN ,oe6?tQwڱ.]-y':mW0#!J82qFjH -`ѓ&M0u Uγmxϵ^-_\])@0Rt.8/?ٰCY]x}=sD3ojަЫNuS%U}ԤwHH>ڗjܷ_3gN q7[q2la*ArǓԖ+p8/RGM ]jacd(JhWko6ڎbj]i5Bj3+3!\j1UZLsLTv8HHmup<>gKMJj0@H%,W΃7R) ">c, xixј^ aܖ>H[i.UIHc U1=yW\=S*GR~)AF=`&2h`DzT󑓶J+?W+}C%P:|0H܆}-<;OC[~o.$~i}~HQ TvXΈr=b}$vizL4:ȰT|4~*!oXQR6Lk+#t/g lԁߖ[Jڶ_N$k*". xsxX7jRVbAAʯKҎU3)zSNN _'s?f)6X!%ssAkʱ>qƷb hg %n ~p1REGMHH=BJiy[<5 ǁJҖgKR*倳e~HUy)Ag,K)`Vw6bRR:qL#\rclK/$sh*$ 6덤 KԖc 3Z9=Ɣ=o>X Ώ"1 )a`SJJ6k(<c e{%kϊP+SL'TcMJWRm ŏ"w)qc ef꒵i?b7b('"2r%~HUS1\<(`1Wx9=8HY9m:X18bgD1u ~|H;K-Uep,, C1 RV.MR5άh,tWO8WC$ XRVsQS]3GJ|12 [vM :k#~tH30Rf-HYݺ-`I9%lIDTm\ S{]9gOڒMNCV\G*2JRŨ;Rҏ^ڽ̱mq1Eu?To3I)y^#jJw^Ńj^vvlB_⋌P4x>0$c>K†Aļ9s_VjTt0l#m>E-,,x,-W)سo&96RE XR.6bXw+)GAEvL)͞K4$p=Ũi_ѱOjb HY/+@θH9޼]Nԥ%n{ &zjT? Ty) s^ULlb,PiTf^<À] 62R^V7)S!nllS6~͝V}-=%* ʻ>G DnK<y&>LPy7'r=Hj 9V`[c"*^8HpcO8bnU`4JȪAƋ#1_\ XϘHPRgik(~G~0DAA_2p|J묭a2\NCr]M_0 ^T%e#vD^%xy-n}-E\3aS%yN!r_{ )sAw ڼp1pEAk~v<:`'ӭ^5 ArXOI驻T (dk)_\ PuA*BY]yB"l\ey hH*tbK)3 IKZ򹞋XjN n *n>k]X_d!ryBH ]*R 0(#'7 %es9??ښFC,ՁQPjARJ\Ρw K#jahgw;2$l*) %Xq5!U᢯6Re] |0[__64ch&_}iL8KEgҎ7 M/\`|.p,~`a=BR?xܐrQ8K XR2M8f ?`sgWS%" Ԉ 7R%$ N}?QL1|-эټwIZ%pvL3Hk>,ImgW7{E xPHx73RA @RS CC !\ȟ5IXR^ZxHл$Q[ŝ40 (>+ _C >BRt<,TrT {O/H+˟Pl6 I B)/VC<6a2~(XwV4gnXR ϱ5ǀHٻ?tw똤Eyxp{#WK qG%5],(0ӈH HZ])ג=K1j&G(FbM@)%I` XRg ʔ KZG(vP,<`[ Kn^ SJRsAʠ5xՅF`0&RbV tx:EaUE/{fi2;.IAwW8/tTxAGOoN?G}l L(n`Zv?pB8K_gI+ܗ #i?ޙ.) p$utc ~DžfՈEo3l/)I-U?aԅ^jxArA ΧX}DmZ@QLےbTXGd.^|xKHR{|ΕW_h] IJ`[G9{).y) 0X YA1]qp?p_k+J*Y@HI>^?gt.06Rn ,` ?);p pSF9ZXLBJPWjgQ|&)7! HjQt<| ؅W5 x W HIzYoVMGP Hjn`+\(dNW)F+IrS[|/a`K|ͻ0Hj{R,Q=\ (F}\WR)AgSG`IsnAR=|8$}G(vC$)s FBJ?]_u XRvύ6z ŨG[36-T9HzpW̞ú Xg큽=7CufzI$)ki^qk-) 0H*N` QZkk]/tnnsI^Gu't=7$ Z;{8^jB% IItRQS7[ϭ3 $_OQJ`7!]W"W,)Iy W AJA;KWG`IY{8k$I$^%9.^(`N|LJ%@$I}ֽp=FB*xN=gI?Q{٥4B)mw $Igc~dZ@G9K X?7)aK%݅K$IZ-`IpC U6$I\0>!9k} Xa IIS0H$I H ?1R.Чj:4~Rw@p$IrA*u}WjWFPJ$I➓/6#! LӾ+ X36x8J |+L;v$Io4301R20M I$-E}@,pS^ޟR[/s¹'0H$IKyfŸfVOπFT*a$I>He~VY/3R/)>d$I>28`Cjw,n@FU*9ttf$I~<;=/4RD~@ X-ѕzἱI$: ԍR a@b X{+Qxuq$IЛzo /~3\8ڒ4BN7$IҀj V]n18H$IYFBj3̵̚ja pp $Is/3R Ӻ-Yj+L;.0ŔI$Av? #!5"aʄj}UKmɽH$IjCYs?h$IDl843.v}m7UiI=&=0Lg0$I4: embe` eQbm0u? $IT!Sƍ'-sv)s#C0:XB2a w I$zbww{."pPzO =Ɔ\[ o($Iaw]`E).Kvi:L*#gР7[$IyGPI=@R 4yR~̮´cg I$I/<tPͽ hDgo 94Z^k盇΄8I56^W$I^0̜N?4*H`237}g+hxoq)SJ@p|` $I%>-hO0eO>\ԣNߌZD6R=K ~n($I$y3D>o4b#px2$yڪtzW~a $I~?x'BwwpH$IZݑnC㧄Pc_9sO gwJ=l1:mKB>Ab<4Lp$Ib o1ZQ@85b̍ S'F,Fe,^I$IjEdù{l4 8Ys_s Z8.x m"+{~?q,Z D!I$ϻ'|XhB)=…']M>5 rgotԎ 獽PH$IjIPhh)n#cÔqA'ug5qwU&rF|1E%I$%]!'3AFD/;Ck_`9 v!ٴtPV;x`'*bQa w I$Ix5 FC3D_~A_#O݆DvV?<qw+I$I{=Z8".#RIYyjǪ=fDl9%M,a8$I$Ywi[7ݍFe$s1ՋBVA?`]#!oz4zjLJo8$I$%@3jAa4(o ;p,,dya=F9ً[LSPH$IJYЉ+3> 5"39aZ<ñh!{TpBGkj}Sp $IlvF.F$I z< '\K*qq.f<2Y!S"-\I$IYwčjF$ w9 \ߪB.1v!Ʊ?+r:^!I$BϹB H"B;L'G[ 4U#5>੐)|#o0aڱ$I>}k&1`U#V?YsV x>{t1[I~D&(I$I/{H0fw"q"y%4 IXyE~M3 8XψL}qE$I[> nD?~sf ]o΁ cT6"?'_Ἣ $I>~.f|'!N?⟩0G KkXZE]ޡ;/&?k OۘH$IRۀwXӨ<7@PnS04aӶp.:@\IWQJ6sS%I$e5ڑv`3:x';wq_vpgHyXZ 3gЂ7{{EuԹn±}$I$8t;b|591nءQ"P6O5i }iR̈́%Q̄p!I䮢]O{H$IRϻ9s֧ a=`- aB\X0"+5"C1Hb?߮3x3&gşggl_hZ^,`5?ߎvĸ%̀M!OZC2#0x LJ0 Gw$I$I}<{Eb+y;iI,`ܚF:5ܛA8-O-|8K7s|#Z8a&><a&/VtbtLʌI$I$I$I$I$I$IRjDD%tEXtdate:create2022-05-31T04:40:26+00:00!Î%tEXtdate:modify2022-05-31T04:40:26+00:00|{2IENDB`Mini Shell

HOME


Mini Shell 1.0
DIR:/usr/share/perl5/Mail/SpamAssassin/
Upload File :
Current File : //usr/share/perl5/Mail/SpamAssassin/DnsResolver.pm
# <@LICENSE>
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements.  See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to you under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License.  You may obtain a copy of the License at:
# 
#     http://www.apache.org/licenses/LICENSE-2.0
# 
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# </@LICENSE>

=head1 NAME

Mail::SpamAssassin::DnsResolver - DNS resolution engine

=head1 DESCRIPTION

This is a DNS resolution engine for SpamAssassin, implemented in order to
reduce file descriptor usage by Net::DNS and avoid a response collision bug in
that module.

=head1 METHODS

=over 4

=cut

# TODO: caching in this layer instead of in callers.

package Mail::SpamAssassin::DnsResolver;

use strict;
use warnings;
# use bytes;
use re 'taint';

require 5.008001;  # needs utf8::is_utf8()

use Mail::SpamAssassin;
use Mail::SpamAssassin::Logger;
use Mail::SpamAssassin::Constants qw(:ip);
use Mail::SpamAssassin::Util qw(untaint_var decode_dns_question_entry);

use Socket;
use Errno qw(EADDRINUSE EACCES);
use Time::HiRes qw(time);

our @ISA = qw();

our $io_socket_module_name;
BEGIN {
  if (eval { require IO::Socket::IP }) {
    $io_socket_module_name = 'IO::Socket::IP';
  } elsif (eval { require IO::Socket::INET6 }) {
    $io_socket_module_name = 'IO::Socket::INET6';
  } elsif (eval { require IO::Socket::INET }) {
    $io_socket_module_name = 'IO::Socket::INET';
  }
}

###########################################################################

sub new {
  my $class = shift;
  $class = ref($class) || $class;

  my ($main) = @_;
  my $self = {
    'main'              => $main,
    'conf'		=> $main->{conf},
    'id_to_callback'    => { },
  };
  bless ($self, $class);

  $self->load_resolver();
  $self;
}

###########################################################################

=item $res->load_resolver()

Load the C<Net::DNS::Resolver> object.  Returns 0 if Net::DNS cannot be used,
1 if it is available.

=cut

sub load_resolver {
  my ($self) = @_;

  if ($self->{res}) { return 1; }
  $self->{no_resolver} = 1;

  # force only ipv4 if no IO::Socket::INET6 or ipv6 doesn't work
  my $force_ipv4 = $self->{main}->{force_ipv4};
  my $force_ipv6 = $self->{main}->{force_ipv6};

  if (!$force_ipv4 && $io_socket_module_name eq 'IO::Socket::INET') {
    dbg("dns: socket module for IPv6 support not available");
    die "Use of IPv6 requested, but not available\n"  if $force_ipv6;
    $force_ipv4 = 1; $force_ipv6 = 0;
  }
  if (!$force_ipv4) {  # test drive IPv6
    eval {
      my $sock6;
      if ($io_socket_module_name) {
        $sock6 = $io_socket_module_name->new(LocalAddr=>'::', Proto=>'udp');
      }
      if ($sock6) { $sock6->close() or warn "error closing socket: $!" }
      $sock6;
    } or do {
      dbg("dns: socket module %s is available, but no host support for IPv6",
          $io_socket_module_name);
      die "Use of IPv6 requested, but not available\n"  if $force_ipv6;
      $force_ipv4 = 1; $force_ipv6 = 0;
    }
  }
  
  eval {
    require Net::DNS;
    # force_v4 is set in new() to avoid error in older versions of Net::DNS
    # that don't have it; other options are set by function calls so a typo
    # or API change will cause an error here
    my $res = $self->{res} = Net::DNS::Resolver->new(force_v4 => $force_ipv4);
    if ($res) {
      $self->{no_resolver} = 0;
      $self->{force_ipv4} = $force_ipv4;
      $self->{force_ipv6} = $force_ipv6;
      $self->{retry} = 1;       # retries for non-backgrounded query
      $self->{retrans} = 3;     # initial timeout for "non-backgrounded"
                                #   query run in background

      $res->retry(1);           # If it fails, it fails
      $res->retrans(0);         # If it fails, it fails
      $res->dnsrch(0);          # ignore domain search-list
      $res->defnames(0);        # don't append stuff to end of query
      $res->tcp_timeout(3);     # timeout of 3 seconds only
      $res->udp_timeout(3);     # timeout of 3 seconds only
      $res->persistent_tcp(0);  # bug 3997
      $res->persistent_udp(0);  # bug 3997

      # RFC 6891 (ex RFC 2671): EDNS0, value is a requestor's UDP payload size
      my $edns = $self->{conf}->{dns_options}->{edns};
      if ($edns && $edns > 512) {
        $res->udppacketsize($edns);
        dbg("dns: EDNS, UDP payload size %d", $edns);
      }

      # set $res->nameservers for the benefit of plugins which don't use
      # our send/bgsend infrastructure but rely on Net::DNS::Resolver entirely
      my @ns_addr_port = $self->available_nameservers();
      local($1,$2);
      # drop port numbers, Net::DNS::Resolver can't take them
      @ns_addr_port = map(/^\[(.*)\]:(\d+)\z/ ? $1 : $_, @ns_addr_port);
      dbg("dns: nameservers set to %s", join(', ', @ns_addr_port));
      $res->nameservers(@ns_addr_port);
    }
    1;
  } or do {
    my $eval_stat = $@ ne '' ? $@ : "errno=$!";  chomp $eval_stat;
    dbg("dns: eval failed: $eval_stat");
  };

  dbg("dns: using socket module: %s version %s%s",
      $io_socket_module_name,
      $io_socket_module_name->VERSION,
      $self->{force_ipv4} ? ', forced IPv4' :
      $self->{force_ipv6} ? ', forced IPv6' : '');
  dbg("dns: is Net::DNS::Resolver available? %s",
      $self->{no_resolver} ? "no" : "yes" );
  if (!$self->{no_resolver} && defined $Net::DNS::VERSION) {
    dbg("dns: Net::DNS version: %s", $Net::DNS::VERSION);
  }

  return (!$self->{no_resolver});
}

=item $resolver = $res->get_resolver()

Return the C<Net::DNS::Resolver> object.

=cut

sub get_resolver {
  my ($self) = @_;
  return $self->{res};
}

=item $res->configured_nameservers()

Get a list of nameservers as configured by dns_server directives
or as provided by Net::DNS, typically from /etc/resolv.conf

=cut

sub configured_nameservers {
  my $self = shift;

  my $res = $self->{res};
  my @ns_addr_port;  # list of name servers: [addr]:port entries
  if ($self->{conf}->{dns_servers}) {  # specified in a config file
    @ns_addr_port = @{$self->{conf}->{dns_servers}};
    dbg("dns: servers set by config to: %s", join(', ',@ns_addr_port));
  } elsif ($res) {  # default as provided by Net::DNS, e.g. /etc/resolv.conf
    my @ns = $res->UNIVERSAL::can('nameservers') ? $res->nameservers
                                                 : @{$res->{nameservers}};
    my $port = $res->UNIVERSAL::can('port') ? $res->port : $res->{port};
    @ns_addr_port = map(untaint_var("[$_]:" . $port), @ns);
    dbg("dns: servers obtained from Net::DNS : %s", join(', ',@ns_addr_port));
  }
  return @ns_addr_port;
}

=item $res->available_nameservers()

Get or set a list of currently available nameservers,
which is typically a known-to-be-good subset of configured nameservers

=cut

sub available_nameservers {
  my $self = shift;

  if (@_) {
    $self->{available_dns_servers} = [ @_ ];  # copy
    dbg("dns: servers set by a caller to: %s",
         join(', ',@{$self->{available_dns_servers}}));
  } elsif (!$self->{available_dns_servers}) {
    # a list of configured name servers: [addr]:port entries
    $self->{available_dns_servers} = [ $self->configured_nameservers() ];
  }
  if ($self->{force_ipv4} || $self->{force_ipv6}) {
    # filter the list according to a chosen protocol family
    my $ip4_re = IPV4_ADDRESS;
    my(@filtered_addr_port);
    for (@{$self->{available_dns_servers}}) {
      local($1,$2);
      /^ \[ (.*) \] : (\d+) \z/xs  or next;
      my($addr,$port) = ($1,$2);
      if ($addr =~ /^${ip4_re}\z/o) {
        push(@filtered_addr_port, $_)  unless $self->{force_ipv6};
      } elsif ($addr =~ /:.*:/) {
        push(@filtered_addr_port, $_)  unless $self->{force_ipv4};
      } else {
        warn "Unrecognized DNS server specification: $_";
      }
    }
    if (@filtered_addr_port < @{$self->{available_dns_servers}}) {
      dbg("dns: filtered DNS servers according to protocol family: %s",
          join(", ",@filtered_addr_port));
    }
    @{$self->{available_dns_servers}} = @filtered_addr_port;
  }
  die "available_nameservers: No DNS servers available!\n"
    if !@{$self->{available_dns_servers}};
  return @{$self->{available_dns_servers}};
}

sub disable_available_port {
  my($self, $lport) = @_;
  if ($lport >= 0 && $lport <= 65535) {
    my $conf = $self->{conf};
    if (!defined $conf->{dns_available_portscount}) {
      $self->pick_random_available_port();  # initialize
    }
    if (vec($conf->{dns_available_ports_bitset}, $lport, 1)) {
      dbg("dns: disabling local port %d", $lport);
      vec($conf->{dns_available_ports_bitset}, $lport, 1) = 0;
      $conf->{dns_available_portscount_buckets}->[$lport >> 8] --;
      $conf->{dns_available_portscount} --;
    }
  }
}

sub pick_random_available_port {
  my $self = shift;
  my $port_number;  # resulting port number, or undef if none available

  my $conf = $self->{conf};
  my $available_portscount = $conf->{dns_available_portscount};

  # initialize when called for the first time or after a config change
  if (!defined $available_portscount) {
    my $ports_bitset = $conf->{dns_available_ports_bitset};
    if (!defined $ports_bitset) {  # ensure it is initialized
      Mail::SpamAssassin::Conf::set_ports_range(\$ports_bitset, 0, 0, 0);
      $conf->{dns_available_ports_bitset} = $ports_bitset;
    }
    # prepare auxilliary data structure to speed up further free-port lookups;
    # 256 buckets, each accounting for 256 ports: 8+8 = 16 bit port numbers;
    # each bucket holds a count of available ports in its range
    my @bucket_counts = (0) x 256;
    my $all_zeroes = "\000" x 32;  # one bucket's worth (256) of zeroes
    my $all_ones   = "\377" x 32;  # one bucket's worth (256) of ones
    my $ind = 0;
    $available_portscount = 0;  # number of all available ports
    foreach my $bucket (0..255) {
      my $cnt = 0;
      my $b = substr($ports_bitset, $bucket*32, 32);  # one bucket: 256 bits
      if  ($b eq $all_zeroes) { $ind += 256 }
      elsif ($b eq $all_ones) { $ind += 256; $cnt += 256 }
      else {  # count nontrivial cases the slow way
        vec($ports_bitset, $ind++, 1) && $cnt++  for 0..255;
      }
      $available_portscount += $cnt;
      $bucket_counts[$bucket] = $cnt;
    }
    $conf->{dns_available_portscount} = $available_portscount;
    if ($available_portscount) {
      $conf->{dns_available_portscount_buckets} = \@bucket_counts;
    } else {  # save some storage
      $conf->{dns_available_portscount_buckets} = undef;
      $conf->{dns_available_ports_bitset} = '';
    }
  }

  # find the n-th port number from the ordered set of available port numbers
  dbg("dns: %d configured local ports for DNS queries", $available_portscount);
  if ($available_portscount > 0) {
    my $ports_bitset = $conf->{dns_available_ports_bitset};
    my $n = int(rand($available_portscount));
    my $bucket_counts_ref = $conf->{dns_available_portscount_buckets};
    my $ind = 0;
    foreach my $bucket (0..255) {
      # find the bucket containing n-th turned-on bit
      my $cnt = $bucket_counts_ref->[$bucket];
      if ($cnt > $n) { last } else { $n -= $cnt; $ind += 256 }
    }
    while ($ind <= 65535) {  # scans one bucket, runs at most 256 iterations
      # find the n-th turned-on bit within the corresponding bucket
      if (vec($ports_bitset, $ind, 1)) {
        if ($n <= 0) { $port_number = $ind; last } else { $n-- }
      }
      $ind++;
    }
  }
  return $port_number;
}

=item $res->connect_sock()

Re-connect to the first nameserver listed in C</etc/resolv.conf> or similar
platform-dependent source, as provided by C<Net::DNS>.

=cut

sub connect_sock {
  my ($self) = @_;

  dbg("dns: connect_sock, resolver: %s", $self->{no_resolver} ? "no" : "yes");
  return if $self->{no_resolver};

  $io_socket_module_name
    or die "No Perl modules for network socket available";

  if ($self->{sock}) {
    $self->{sock}->close()
      or info("connect_sock: error closing socket %s: %s", $self->{sock}, $!);
    $self->{sock} = undef;
  }
  my $sock;
  my $errno;

  # list of name servers: [addr]:port entries
  my @ns_addr_port = $self->available_nameservers();
  # use the first name server in a list
  my($ns_addr,$ns_port); local($1,$2);
  ($ns_addr,$ns_port) = ($1,$2)  if $ns_addr_port[0] =~ /^\[(.*)\]:(\d+)\z/;

  # Ensure families of src and dest addresses match (bug 4412 comment 29).
  # Older IO::Socket::INET6 may choose a wrong LocalAddr if protocol family
  # is unspecified, causing EINVAL failure when automatically assigned local
  # IP address and a remote address do not belong to the same address family.
  # Let's choose a suitable source address if possible.
  my $ip4_re = IPV4_ADDRESS;
  my $srcaddr;
  if ($self->{force_ipv4}) {
    $srcaddr = "0.0.0.0";
  } elsif ($self->{force_ipv6}) {
    $srcaddr = "::";
  } elsif ($ns_addr =~ /^${ip4_re}\z/o) {
    $srcaddr = "0.0.0.0";
  } elsif ($ns_addr =~ /:.*:/) {
    $srcaddr = "::";
  } else {  # unrecognized
    # unspecified address, unspecified protocol family
  }

  # find a free local random port from a set of declared-to-be-available ports
  my $lport;
  my $attempts = 0;
  for (;;) {
    $attempts++;
    $lport = $self->pick_random_available_port();
    if (!defined $lport) {
      $lport = 0;
      dbg("no configured local ports for DNS queries, letting OS choose");
    }
    if ($attempts+1 > 50) {  # sanity check
      warn "could not create a DNS resolver socket in $attempts attempts\n";
      $errno = 0;
      last;
    }
    dbg("dns: LocalAddr: [%s]:%d, name server: [%s]:%d, module %s",
        $srcaddr||'x', $lport,  $ns_addr, $ns_port,  $io_socket_module_name);
    my %args = (
        PeerAddr => $ns_addr,
        PeerPort => $ns_port,
        LocalAddr => $srcaddr,
        LocalPort => $lport,
        Type => SOCK_DGRAM,
        Proto => 'udp',
    );
    $sock = $io_socket_module_name->new(%args);

    last if $sock;  # ok, got it

    # IO::Socket::IP constructor provides full error messages in $@
    $errno = $io_socket_module_name eq 'IO::Socket::IP' ? $@ : $!;

    if ($! == EADDRINUSE || $! == EACCES) {
      # in use, let's try another source port
      dbg("dns: UDP port $lport already in use, trying another port");
      if ($self->{conf}->{dns_available_portscount} > 100) {  # still abundant
        $self->disable_available_port($lport);
      }
    } else {
      warn "error creating a DNS resolver socket: $errno";
      goto no_sock;
    }
  }
  if (!$sock) {
    warn "could not create a DNS resolver socket in $attempts attempts: $errno";
    goto no_sock;
  }

  eval {
    my($bufsiz,$newbufsiz);
    $bufsiz = $sock->sockopt(Socket::SO_RCVBUF)
      or die "cannot get a resolver socket rx buffer size: $!";
    if ($bufsiz >= 32*1024) {
      dbg("dns: resolver socket rx buffer size is %d bytes, local port %d",
           $bufsiz, $lport);
    } else {
      $sock->sockopt(Socket::SO_RCVBUF, 32*1024)
        or die "cannot set a resolver socket rx buffer size: $!";
      $newbufsiz = $sock->sockopt(Socket::SO_RCVBUF)
        or die "cannot get a resolver socket rx buffer size: $!";
      dbg("dns: resolver socket rx buffer size changed from %d to %d bytes, ".
          "local port %d", $bufsiz, $newbufsiz, $lport);
    }
    1;
  } or do {
    my $eval_stat = $@ ne '' ? $@ : "errno=$!";  chomp $eval_stat;
    info("dns: socket buffer size error: $eval_stat");
  };

  $self->{sock} = $sock;
  $self->{sock_as_vec} = $self->fhs_to_vec($self->{sock});
  return;

no_sock:
  undef $self->{sock};
  undef $self->{sock_as_vec};
}

sub connect_sock_if_reqd {
  my ($self) = @_;
  $self->connect_sock() if !$self->{sock};
}

=item $res->get_sock()

Return the C<IO::Socket::INET> object used to communicate with
the nameserver.

=cut

sub get_sock {
  my ($self) = @_;
  $self->connect_sock_if_reqd();
  return $self->{sock};
}

###########################################################################

=item $packet = new_dns_packet ($domain, $type, $class)

A wrapper for C<Net::DNS::Packet::new()> which traps a die thrown by it.

To use this, change calls to C<Net::DNS::Resolver::bgsend> from:

    $res->bgsend($domain, $type);

to:

    $res->bgsend(Mail::SpamAssassin::DnsResolver::new_dns_packet($domain, $type, $class));

=cut

# implements draft-vixie-dnsext-dns0x20-00
#
sub dnsext_dns0x20 {
  my ($string) = @_;
  my $rnd;
  my $have_rnd_bits = 0;
  my $result = '';
  for my $ic (unpack("C*",$string)) {
    if (chr($ic) =~ /^[A-Za-z]\z/) {
      if ($have_rnd_bits < 1) {
        # only reveal few bits at a time, hiding most of the accumulator
        $rnd = int(rand(0x7fffffff)) & 0xff;  $have_rnd_bits = 8;
      }
      $ic ^= 0x20  if $rnd & 1;  # flip the 0x20 bit in name if dice says so
      $rnd = $rnd >> 1;  $have_rnd_bits--;
    }
    $result .= chr($ic);
  }
  return $result;
}

# this subroutine mimics the Net::DNS::Resolver::Base::make_query_packet()
#
sub new_dns_packet {
  my ($self, $domain, $type, $class) = @_;

  return if $self->{no_resolver};

  # construct a PTR query if it looks like an IPv4 address
  if (!defined($type) || $type eq 'PTR') {
    local($1,$2,$3,$4);
    if ($domain =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/) {
      $domain = "$4.$3.$2.$1.in-addr.arpa.";
      $type = 'PTR';
    }
  }
  $type  = 'A'   if !defined $type;   # a Net::DNS::Packet default
  $class = 'IN'  if !defined $class;  # a Net::DNS::Packet default

  my $packet;
  eval {

    if (utf8::is_utf8($domain)) {  # since Perl 5.8.1
      info("dns: new_dns_packet: domain is utf8 flagged: %s", $domain);
    }

    $domain =~ s/\.*\z/./s;
    if (length($domain) > 255) {
      die "domain name longer than 255 bytes\n";
    } elsif ($domain !~ /^ (?: [^.]{1,63} \. )+ \z/sx) {
      if ($domain !~ /^ (?: [^.]+ \. )+ \z/sx) {
        die "a domain name contains a null label\n";
      } else {
        die "a label in a domain name is longer than 63 bytes\n";
      }
    }

    if ($self->{conf}->{dns_options}->{dns0x20}) {
      $domain = dnsext_dns0x20($domain);
    } else {
      $domain =~ tr/A-Z/a-z/;  # lowercase, limited to plain ASCII
    }

    # Net::DNS expects RFC 1035 zone format encoding even in its API, silly!
    # Since 0.68 it also assumes that domain names containing characters
    # with codes above 0177 imply that IDN translation is to be performed.
    # Protect also nonprintable characters just in case, ensuring transparency.
    $domain =~ s{ ( [\000-\037\177-\377\\] ) }
                { $1 eq '\\' ? "\\$1" : sprintf("\\%03d",ord($1)) }xgse;

    $packet = Net::DNS::Packet->new($domain, $type, $class);

    # a bit noisy, so commented by default...
    #dbg("dns: new DNS packet time=%.3f domain=%s type=%s id=%s",
    #    time, $domain, $type, $packet->id);
    1;
  } or do {
    # get here if a domain name in a query is invalid, or if a timeout signal
    # happened to be trapped by this eval, or if Net::DNS signalled an error
    my $eval_stat = $@ ne '' ? $@ : "errno=$!";  chomp $eval_stat;
    # resignal if alarm went off
    die "dns: (1) $eval_stat\n"  if $eval_stat =~ /__alarm__ignore__\(.*\)/s;
    warn sprintf(
           "dns: new_dns_packet (domain=%s type=%s class=%s) failed: %s\n",
           $domain, $type, $class, $eval_stat);
  };

  if ($packet) {
    # RD flag needs to be set explicitly since Net::DNS 1.01, Bug 7223	
    $packet->header->rd(1);

  # my $udp_payload_size = $self->{res}->udppacketsize;
    my $udp_payload_size = $self->{conf}->{dns_options}->{edns};
    if ($udp_payload_size && $udp_payload_size > 512) {
    # dbg("dns: adding EDNS ext, UDP payload size %d", $udp_payload_size);
      if ($packet->UNIVERSAL::can('edns')) {  # available since Net::DNS 0.69
        $packet->edns->size($udp_payload_size);
      } else {  # legacy mechanism
        my $optrr = Net::DNS::RR->new(Type => 'OPT', Name => '', TTL => 0,
                                      Class => $udp_payload_size);
        $packet->push('additional', $optrr);
      }
    }
  }

  return $packet;
}

# Internal function used only in this file
## compute a unique ID for a packet to match the query to the reply
## It must use only data that is returned unchanged by the nameserver.
## Argument is a Net::DNS::Packet that has a non-empty question section,
## return is an (opaque) string that can be used as a hash key
sub _packet_id {
  my ($self, $packet) = @_;
  my $header = $packet->header;
  my $id = $header->id;
  my @questions = $packet->question;

  @questions <= 1
    or warn "dns: packet has multiple questions: " . $packet->string . "\n";

  if ($questions[0]) {
    # Bug 6232: Net::DNS::Packet::new is not consistent in keeping data in
    # sections of a packet either as original bytes or presentation-encoded:
    # creating a query packet as above in new_dns_packet() keeps label in
    # non-encoded form, yet on parsing an answer packet, its query section
    # is converted to presentation form by Net::DNS::Question::parse calling
    # Net::DNS::Packet::dn_expand and Net::DNS::wire2presentation in turn.
    # Let's undo the effect of the wire2presentation routine here to make
    # sure the query section of an answer packet matches the query section
    # in our packet as formed by new_dns_packet():
    #
    my($class,$type,$qname) = decode_dns_question_entry($questions[0]);
    $qname =~ tr/A-Z/a-z/  if !$self->{conf}->{dns_options}->{dns0x20};
    return join('/', $id, $class, $type, $qname);

  } else {
    # Odd, this should not happen, a DNS servers is supposed to retain
    # a question section in its reply.  There is a bug in Net::DNS 0.72
    # and earlier where a signal (e.g. a timeout alarm) during decoding
    # of a reply packet produces a seemingly valid packet object, but
    # with missing sections - see [rt.cpan.org #83451] .
    #
    # Better support it; just return the (safe) ID part, along with
    # a text token indicating that the packet had no question part.
    #
    return $id . "/NO_QUESTION_IN_PACKET";
  }
}

###########################################################################

=item $id = $res->bgsend($domain, $type, $class, $cb)

Quite similar to C<Net::DNS::Resolver::bgsend>, except that when a reply
packet eventually arrives, and C<poll_responses> is called, the callback
sub reference C<$cb> will be called.

Note that C<$type> and C<$class> may be C<undef>, in which case they
will default to C<A> and C<IN>, respectively.

The callback sub will be called with three arguments -- the packet that was
delivered, and an id string that fingerprints the query packet and the expected
reply. The third argument is a timestamp (Unix time, floating point), captured
at the time the packet was collected. It is expected that a closure callback
be used, like so:

  my $id = $self->{resolver}->bgsend($domain, $type, undef, sub {
        my ($reply, $reply_id, $timestamp) = @_;
        $self->got_a_reply ($reply, $reply_id);
      });

The callback can ignore the reply as an invalid packet sent to the listening
port if the reply id does not match the return value from bgsend.

=cut

sub bgsend {
  my ($self, $domain, $type, $class, $cb) = @_;
  return if $self->{no_resolver};

  $self->{send_timed_out} = 0;

  my $pkt = $self->new_dns_packet($domain, $type, $class);
  return if !$pkt;  # just bail out, new_dns_packet already reported a failure

  my @ns_addr_port = $self->available_nameservers();
  dbg("dns: bgsend, DNS servers: %s", join(', ',@ns_addr_port));
  my $n_servers = scalar @ns_addr_port;

  my $ok;
  for (my $attempts=1; $attempts <= $n_servers; $attempts++) {
    dbg("dns: attempt %d/%d, trying connect/sendto to %s",
        $attempts, $n_servers, $ns_addr_port[0]);
    $self->connect_sock_if_reqd();
    if ($self->{sock} && defined($self->{sock}->send($pkt->data, 0))) {
      $ok = 1; last;
    } else {  # any other DNS servers in a list to try?
      my $msg = !$self->{sock} ? "unable to connect to $ns_addr_port[0]"
                               : "sendto() to $ns_addr_port[0] failed: $!";
      $self->finish_socket();
      if ($attempts >= $n_servers) {
        warn "dns: $msg, no more alternatives\n";
        last;
      }
      # try with a next DNS server, rotate the list left
      warn "dns: $msg, failing over to $ns_addr_port[1]\n";
      push(@ns_addr_port, shift(@ns_addr_port));
      $self->available_nameservers(@ns_addr_port);
    }
  }
  return if !$ok;
  my $id = $self->_packet_id($pkt);
  dbg("dns: providing a callback for id: $id");
  $self->{id_to_callback}->{$id} = $cb;
  return $id;
}

###########################################################################

=item $id = $res->bgread()

Similar to C<Net::DNS::Resolver::bgread>.  Reads a DNS packet from
a supplied socket, decodes it, and returns a Net::DNS::Packet object
if successful.  Dies on error.

=cut

sub bgread {
  my ($self) = @_;
  my $sock = $self->{sock};
  my $packetsize = $self->{res}->udppacketsize;
  $packetsize = 512  if $packetsize < 512;  # just in case
  my $data = '';
  my $peeraddr = $sock->recv($data, $packetsize+256);  # with some size margin for troubleshooting
  defined $peeraddr or die "bgread: recv() failed: $!";
  my $peerhost = $sock->peerhost;
  $data ne '' or die "bgread: received empty packet from $peerhost";
  dbg("dns: bgread: received %d bytes from %s", length($data), $peerhost);
  my($answerpkt, $decoded_length) = Net::DNS::Packet->new(\$data);
  $answerpkt or die "bgread: decoding DNS packet failed: $@";
  $answerpkt->answerfrom($peerhost);
  if (defined $decoded_length && $decoded_length ne "" && $decoded_length != length($data)) {
    warn sprintf("bgread: received a %d bytes packet from %s, decoded %d bytes\n",
                 length($data), $peerhost, $decoded_length);
  }
  return $answerpkt;
}

###########################################################################

=item $nfound = $res->poll_responses()

See if there are any C<bgsend> reply packets ready, and return
the number of such packets delivered to their callbacks.

=cut

sub poll_responses {
  my ($self, $timeout) = @_;
  return if $self->{no_resolver};
  return if !$self->{sock};
  my $cnt = 0;

  my $rin = $self->{sock_as_vec};
  my $rout;

  for (;;) {
    my ($nfound, $timeleft, $eval_stat);
    eval {  # use eval to catch alarm signal
      my $timer;  # collects timestamp when variable goes out of scope
      if (!defined($timeout) || $timeout > 0)
        { $timer = $self->{main}->time_method("poll_dns_idle") }
      $! = 0;
      ($nfound, $timeleft) = select($rout=$rin, undef, undef, $timeout);
      1;
    } or do {
      $eval_stat = $@ ne '' ? $@ : "errno=$!";  chomp $eval_stat;
    };
    if (defined $eval_stat) {
      # most likely due to an alarm signal, resignal if so
      die "dns: (2) $eval_stat\n"  if $eval_stat =~ /__alarm__ignore__\(.*\)/s;
      warn "dns: select aborted: $eval_stat\n";
      return;
    } elsif (!defined $nfound || $nfound < 0) {
      if ($!) { warn "dns: select failed: $!\n" }
      else    { info("dns: select interrupted") }  # shouldn't happen
      return;
    } elsif (!$nfound) {
      if (!defined $timeout) { warn("dns: select returned empty-handed\n") }
      elsif ($timeout > 0) { dbg("dns: select timed out %.3f s", $timeout) }
      return;
    }

    my $now = time;
    $timeout = 0;  # next time around collect whatever is available, then exit
    last  if $nfound == 0;

    my $packet;
    # Bug 7265, use our own bgread() below
    # $packet = $self->{res}->bgread($self->{sock});
    eval {
      $packet = $self->bgread();  # Bug 7265, use our own bgread()
    } or do {
      undef $packet;
      my $eval_stat = $@ ne '' ? $@ : "errno=$!";  chomp $eval_stat;
      # resignal if alarm went off
      die $eval_stat  if $eval_stat =~ /__alarm__ignore__\(.*\)/s;
      info("dns: bad dns reply: %s", $eval_stat);
    };

    if (!$packet) {
      # error already reported above
#     my $dns_err = $self->{res}->errorstring;
#     die "dns (3) $dns_err\n"  if $dns_err =~ /__alarm__ignore__\(.*\)/s;
#     info("dns: bad dns reply: $dns_err");
    } else {
      my $header = $packet->header;
      if (!$header) {
        info("dns: dns reply is missing a header section");
      } else {
        my $rcode = $header->rcode;
        my $packet_id = $header->id;
        my $id = $self->_packet_id($packet);

        if ($rcode eq 'NOERROR') {  # success
          # NOERROR, may or may not have answer records
          dbg("dns: dns reply %s is OK, %d answer records",
              $packet_id, $header->ancount);
          if ($header->tc) {  # truncation flag turned on
            my $edns = $self->{conf}->{dns_options}->{edns} || 512;
            info("dns: reply to %s truncated (%s), %d answer records", $id,
                 $edns == 512 ? "EDNS off" : "EDNS $edns bytes",
                 $header->ancount);
          }
        } else {
          # some failure, e.g. NXDOMAIN, SERVFAIL, FORMERR, REFUSED, ...
          # btw, one reason for SERVFAIL is an RR signature failure in DNSSEC
          dbg("dns: dns reply to %s: %s", $id, $rcode);
        }

        # A hash lookup: the id must match exactly (case-sensitively).
        # The domain name part of the id was lowercased if dns0x20 is off,
        # and case-randomized when dns0x20 option is on.
        #
        my $cb = delete $self->{id_to_callback}->{$id};

        if ($cb) {
          $cb->($packet, $id, $now);
          $cnt++;
        } else {  # no match, report the problem
          if ($rcode eq 'REFUSED' || $id =~ m{^\d+/NO_QUESTION_IN_PACKET\z}) {
            # the failure was already reported above
          } else {
            info("dns: no callback for id %s, ignored; packet: %s",
                 $id,  $packet ? $packet->string : "undef" );
          }
          # report a likely matching query for diagnostic purposes
          local $1;
          if ($id =~ m{^(\d+)/}) {
            my $dnsid = $1;  # the raw DNS packet id
            my @matches =
              grep(m{^\Q$dnsid\E/}, keys %{$self->{id_to_callback}});
            if (!@matches) {
              info("dns: no likely matching queries for id %s", $dnsid);
            } else {
              info("dns: a likely matching query: %s", join(', ', @matches));
            }
          }
        }
      }
    }
  }

  return $cnt;
}

###########################################################################

=item $res->bgabort()

Call this to release pending requests from memory, when aborting backgrounded
requests, or when the scan is complete.
C<Mail::SpamAssassin::PerMsgStatus::check> calls this before returning.

=cut

sub bgabort {
  my ($self) = @_;
  $self->{id_to_callback} = {};
}

###########################################################################

=item $packet = $res->send($name, $type, $class)

Emulates C<Net::DNS::Resolver::send()>.

This subroutine is a simple synchronous leftover from SpamAssassin version
3.3 and does not participate in packet query caching and callback grouping
as implemented by AsyncLoop::bgsend_and_start_lookup().  As such it should
be avoided for mainstream usage.  Currently used through Mail::SPF::Server
by the SPF plugin.

=cut

sub send {
  my ($self, $name, $type, $class) = @_;
  return if $self->{no_resolver};

  # Avoid passing utf8 character strings to DNS, as it has no notion of
  # character set encodings - encode characters somehow to plain bytes
  # using some arbitrary encoding (they are normally just 7-bit ascii
  # characters anyway, just need to get rid of the utf8 flag).  Bug 6959
  # Most if not all af these come from a SPF plugin.
  #
  utf8::encode($name);

  my $retrans = $self->{retrans};
  my $retries = $self->{retry};
  my $timeout = $retrans;
  my $answerpkt;
  my $answerpkt_avail = 0;
  for (my $i = 0;
       (($i < $retries) && !defined($answerpkt));
       ++$i, $retrans *= 2, $timeout = $retrans) {

    $timeout = 1 if ($timeout < 1);
    # note nifty use of a closure here.  I love closures ;)
    my $id = $self->bgsend($name, $type, $class, sub {
      my ($reply, $reply_id, $timestamp) = @_;
      $answerpkt = $reply; $answerpkt_avail = 1;
    });

    last if !defined $id;  # perhaps a restricted zone or a serious failure

    my $now = time;
    my $deadline = $now + $timeout;

    while (!$answerpkt_avail) {
      if ($now >= $deadline) { $self->{send_timed_out} = 1; last }
      $self->poll_responses(1);
      $now = time;
    }
  }
  return $answerpkt;
}

###########################################################################

=item $res->errorstring()

Little more than a stub for callers expecting this from C<Net::DNS::Resolver>.

If called immediately after a call to $res->send this will return
C<query timed out> if the $res->send DNS query timed out.  Otherwise 
C<unknown error or no error> will be returned.

No other errors are reported.

=cut

sub errorstring {
  my ($self) = @_;
  return 'query timed out' if $self->{send_timed_out};
  return 'unknown error or no error';
}

###########################################################################

=item $res->finish_socket()

Reset socket when done with it.

=cut

sub finish_socket {
  my ($self) = @_;
  if ($self->{sock}) {
    $self->{sock}->close()
      or warn "finish_socket: error closing socket $self->{sock}: $!";
    undef $self->{sock};
  }
}

###########################################################################

=item $res->finish()

Clean up for destruction.

=cut

sub finish {
  my ($self) = @_;
  $self->finish_socket();
  %{$self} = ();
}

###########################################################################
# non-public methods.

# should move to Util.pm (TODO)
sub fhs_to_vec {
  my ($self, @fhlist) = @_;
  my $rin = '';
  foreach my $sock (@fhlist) {
    my $fno = fileno($sock);
    if (!defined $fno) {
      warn "dns: oops! fileno now undef for $sock";
    } else {
      vec ($rin, $fno, 1) = 1;
    }
  }
  return $rin;
}

# call Mail::SA::init() instead
sub reinit_post_fork {
  my ($self) = @_;
  # release parent's socket, don't want all spamds sharing the same socket
  $self->finish_socket();
}

1;

=back

=cut