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/Plugin/
Upload File :
Current File : //usr/share/perl5/Mail/SpamAssassin/Plugin/DCC.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>

# Changes since SpamAssassin 3.3.2:
#   support for DCC learning.  See dcc_learn_score.
#   deal with orphan dccifd sockets
#   use `cdcc -q` to not stall waiting to find a DCC server when deciding
#     whether DCC checks are enabled
#   use dccproc -Q or dccifd query if a pre-existing X-DCC header shows
#     the message has already been reported
#   dccproc now uses -w /var/dcc/whiteclnt so it acts more like dccifd
#   warn about the use of ancient versions of dccproc and dccifd
#   turn off dccifd greylisting
#   query instead of reporting mail messages that contain X-DCC headers
#     and so has probably already been reported
#   try harder to find dccproc and cdcc when not explicitly configured
#	Rhyolite Software DCC 2.3.140-1.4 $Revision$

=head1 NAME

Mail::SpamAssassin::Plugin::DCC - perform DCC check of messages

=head1 SYNOPSIS

  loadplugin Mail::SpamAssassin::Plugin::DCC

  full DCC_CHECK	eval:check_dcc()
  full DCC_CHECK_50_79	eval:check_dcc_reputation_range('50','79')

=head1 DESCRIPTION

The DCC or Distributed Checksum Clearinghouse is a system of servers
collecting and counting checksums of millions of mail messages.
The counts can be used by SpamAssassin to detect and filter spam.

See http://www.dcc-servers.net/dcc/ for more information about DCC.

Note that DCC is disabled by default in C<v310.pre> because its use requires
software that is not distributed with SpamAssassin and that has license
restrictions for certain commercial uses.
See the DCC license at http://www.dcc-servers.net/dcc/LICENSE for details.

Enable it by uncommenting the "loadplugin Mail::SpamAssassin::Plugin::DCC"
confdir/v310.pre or by adding this line to your local.pre.  It might also
be necessary to install a DCC package, port, rpm, or equivalent from your
operating system distributor or a tarball from the primary DCC source
at http://www.dcc-servers.net/dcc/#download
See also http://www.dcc-servers.net/dcc/INSTALL.html

=head1 TAGS

The following tags are added to the set, available for use in reports,
header fields, other plugins, etc.:

  _DCCB_    DCC server ID in X-DCC-*-Metrics header field name
  _DCCR_    X-DCC-*-Metrics header field body
  _DCCREP_  DCC Reputation or percent bulk mail (0..100) from
	      commercial DCC software

=cut

package Mail::SpamAssassin::Plugin::DCC;

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

use Mail::SpamAssassin::Plugin;
use Mail::SpamAssassin::Logger;
use Mail::SpamAssassin::Timeout;
use Mail::SpamAssassin::Util qw(untaint_var untaint_file_path
                                proc_status_ok exit_status_str);
use Errno qw(ENOENT EACCES);
use IO::Socket;

our @ISA = qw(Mail::SpamAssassin::Plugin);

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;
  my $mailsaobject = shift;

  $class = ref($class) || $class;
  my $self = $class->SUPER::new($mailsaobject);
  bless ($self, $class);

  # are network tests enabled?
  if ($mailsaobject->{local_tests_only}) {
    $self->{use_dcc} = 0;
    dbg("dcc: local tests only, disabling DCC");
  }
  else {
    dbg("dcc: network tests on, registering DCC");
  }

  $self->register_eval_rule("check_dcc");
  $self->register_eval_rule("check_dcc_reputation_range");

  $self->set_config($mailsaobject->{conf});

  return $self;
}

sub set_config {
  my($self, $conf) = @_;
  my @cmds;

=head1 USER OPTIONS

=over 4

=item use_dcc (0|1)		(default: 1)

Whether to use DCC, if it is available.

=cut

  push(@cmds, {
    setting => 'use_dcc',
    default => 1,
    type => $Mail::SpamAssassin::Conf::CONF_TYPE_BOOL,
  });

=item dcc_body_max NUMBER

=item dcc_fuz1_max NUMBER

=item dcc_fuz2_max NUMBER

Sets how often a message's body/fuz1/fuz2 checksum must have been reported
to the DCC server before SpamAssassin will consider the DCC check hit.
C<999999> is DCC's MANY count.

The default is C<999999> for all these options.

=item dcc_rep_percent NUMBER

Only the commercial DCC software provides DCC Reputations.  A DCC Reputation
is the percentage of bulk mail received from the last untrusted relay in the
path taken by a mail message as measured by all commercial DCC installations.
See http://www.rhyolite.com/dcc/reputations.html
You C<must> whitelist your trusted relays or MX servers with MX or
MXDCC lines in /var/dcc/whiteclnt as described in the main DCC man page
to avoid seeing your own MX servers as sources of bulk mail.
See http://www.dcc-servers.net/dcc/dcc-tree/dcc.html#White-and-Blacklists
The default is C<90>.

=cut

  push (@cmds, {
    setting => 'dcc_body_max',
    default => 999999,
    type => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC
  },
  {
    setting => 'dcc_fuz1_max',
    default => 999999,
    type => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC
  },
  {
    setting => 'dcc_fuz2_max',
    default => 999999,
    type => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC
  },
  {
    setting => 'dcc_rep_percent',
    default => 90,
    type => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC
  });

=back

=head1 ADMINISTRATOR OPTIONS

=over 4

=item dcc_timeout n		(default: 8)

How many seconds you wait for DCC to complete, before scanning continues
without the DCC results. A numeric value is optionally suffixed by a
time unit (s, m, h, d, w, indicating seconds (default), minutes, hours,
days, weeks).

=cut

  push (@cmds, {
    setting => 'dcc_timeout',
    is_admin => 1,
    default => 8,
    type => $Mail::SpamAssassin::Conf::CONF_TYPE_DURATION,
  });

=item dcc_home STRING

This option tells SpamAssassin where to find the dcc homedir.
If not specified, try to use the locally configured directory
from the C<cdcc homedir> command.
Try /var/dcc if that command fails.

=cut

  push (@cmds, {
    setting => 'dcc_home',
    is_admin => 1,
    type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING,
    code => sub {
      my ($self, $key, $value, $line) = @_;
      if (!defined $value || $value eq '') {
	return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
      }
      $value = untaint_file_path($value);
      my $stat_errn = stat($value) ? 0 : 0+$!;
      if ($stat_errn != 0 || !-d _) {
	my $msg = $stat_errn == ENOENT ? "does not exist"
		  : !-d _ ? "is not a directory" : "not accessible: $!";
	info("config: dcc_home \"$value\" $msg");
	return $Mail::SpamAssassin::Conf::INVALID_VALUE;
      }

      $self->{dcc_home} = $value;
    }
  });

=item dcc_dccifd_path STRING

This option tells SpamAssassin where to find the dccifd socket instead
of a local Unix socket named C<dccifd> in the C<dcc_home> directory.
If a socket is specified or found, use it instead of C<dccproc>.

If specified, C<dcc_dccifd_path> is the absolute path of local Unix socket
or an INET socket specified as C<[Host]:Port> or C<Host:Port>.
Host can be an IPv4 or IPv6 address or a host name
Port is a TCP port number. The brackets are required for an IPv6 address.

The default is C<undef>.

=cut

  push (@cmds, {
    setting => 'dcc_dccifd_path',
    is_admin => 1,
    type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING,
    code => sub {
      my ($self, $key, $value, $line) = @_;

      if (!defined $value || $value eq '') {
	return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
      }

      local($1,$2,$3);
      if ($value =~ m{^ (?: \[ ([^\]]*) \] | ([^:]*) ) : ([^:]*) \z}sx) {
	my $host = untaint_var(defined $1 ? $1 : $2);
	my $port = untaint_var($3);
	if (!$host) {
	  info("config: missing or bad host name in dcc_dccifd_path '$value'");
	  return $Mail::SpamAssassin::Conf::INVALID_VALUE;
	}
	if (!$port || $port !~ /^\d+\z/ || $port < 1 || $port > 65535) {
	  info("config: bad TCP port number in dcc_dccifd_path '$value'");
	  return $Mail::SpamAssassin::Conf::INVALID_VALUE;
	}

	$self->{dcc_dccifd_host} = $host;
	$self->{dcc_dccifd_port} = $port;
	dbg("config: dcc_dccifd_path set to [%s]:%s", $host, $port);

      } else {
	# assume a unix socket
	if ($value !~ m{^/}) {
	  info("config: dcc_dccifd_path '$value' is not an absolute path");
	  # return $Mail::SpamAssassin::Conf::INVALID_VALUE;  # abort or accept?
	}
	$value = untaint_file_path($value);

	$self->{dcc_dccifd_socket} = $value;
	dbg("config: dcc_dccifd_path set to local socket %s", $value);
	dbg("dcc: dcc_dccifd_path set to local socket %s", $value);
      }

      $self->{dcc_dccifd_path_raw} = $value;
    }
  });

=item dcc_path STRING

Where to find the C<dccproc> client program instead of relying on SpamAssassin
to find it in the current PATH or C<dcc_home/bin>. This must often be set,
because the current PATH is cleared by I<taint mode> in the Perl interpreter,

If a C<dccifd> socket is found in C<dcc_home> or specified explicitly
with C<dcc_dccifd_path>, use the C<dccifd(8)> interface instead of C<dccproc>.

The default is C<undef>.


=cut

  push (@cmds, {
    setting => 'dcc_path',
    is_admin => 1,
    default => undef,
    type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING,
    code => sub {
      my ($self, $key, $value, $line) = @_;
      if (!defined $value || $value eq '') {
	return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
      }
      $value = untaint_file_path($value);
      if (!-x $value) {
	info("config: dcc_path '$value' is not executable");
	return $Mail::SpamAssassin::Conf::INVALID_VALUE;
      }

      $self->{dcc_path} = $value;
    }
  });

=item dcc_options options

Specify additional options to the dccproc(8) command.  Only
characters in the range [0-9A-Za-z ,._/-] are allowed for security reasons.

The default is C<undef>.

=cut

  push (@cmds, {
    setting => 'dcc_options',
    is_admin => 1,
    default => undef,
    type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING,
    code => sub {
      my ($self, $key, $value, $line) = @_;
      if ($value !~ m{^([0-9A-Za-z ,._/-]+)$}) {
	info("config: dcc_options '$value' contains impermissible characters");
	return $Mail::SpamAssassin::Conf::INVALID_VALUE;
      }
      $self->{dcc_options} = $1;
    }
  });

=item dccifd_options options

Specify additional options to send to the dccifd daemon with
the ASCII protocol described on the dccifd(8) man page.
Only characters in the range [0-9A-Za-z ,._/-] are allowed for security reasons.

The default is C<undef>.

=cut

  push (@cmds, {
    setting => 'dccifd_options',
    is_admin => 1,
    default => undef,
    type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING,
    code => sub {
      my ($self, $key, $value, $line) = @_;
      if ($value !~ m{^([0-9A-Za-z ,._/-]+)$}) {
	info("config: dccifd_options '$value' contains impermissible characters");
	return $Mail::SpamAssassin::Conf::INVALID_VALUE;
      }
      $self->{dccifd_options} = $1;
    }
  });

=item dcc_learn_score n		(default: undef)

Report messages with total scores this much larger than the
SpamAssassin spam threshold to DCC as spam.

=back

=cut

  push (@cmds, {
    setting => 'dcc_learn_score',
    is_admin => 1,
    default => undef,
    type => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC,
  });

  $conf->{parser}->register_commands(\@cmds);
}




sub ck_dir {
  my ($self, $dir, $tgt, $src) = @_;

  $dir = untaint_file_path($dir);
  if (!stat($dir)) {
    my $dir_errno = 0+$!;
    if ($dir_errno == ENOENT) {
      dbg("dcc: $tgt $dir from $src does not exist");
    } else {
      dbg("dcc: $tgt $dir from $src is not accessible: $!");
    }
    return;
  }
  if (!-d _) {
    dbg("dcc: $tgt $dir from $src is not a directory");
    return;
  }

  $self->{main}->{conf}->{$tgt} = $dir;
  dbg("dcc: use '$tgt $dir' from $src");
}

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

  # just once
  return if defined $self->{dcc_version};
  $self->{dcc_version} = '?';

  my $conf = $self->{main}->{conf};


  # Get the DCC software version for talking to dccifd and formating the
  # dccifd options and the built-in DCC homedir.  Use -q to prevent delays.
  my $cdcc_home;
  my $cdcc = $self->dcc_pgm_path('cdcc');
  my $cmd = '-qV homedir libexecdir';
  if ($cdcc && open(CDCC, "$cdcc $cmd 2>&1 |")) {
    my $cdcc_output = do { local $/ = undef; <CDCC> };
    close CDCC;

    $cdcc_output =~ s/\n/ /g;		# everything in 1 line for debugging
    dbg("dcc: `%s %s` reports '%s'", $cdcc, $cmd, $cdcc_output);
    $self->{dcc_version} = ($cdcc_output =~ /^(\d+\.\d+\.\d+)/) ? $1 : '';
    $cdcc_home = ($cdcc_output =~ /\s+homedir=(\S+)/) ? $1 : '';
    if ($cdcc_output =~ /\s+libexecdir=(\S+)/) {
      $self->ck_dir($1, 'dcc_libexec', 'cdcc');
    }
  }

  # without a home, try the homedir from cdcc
  if (!$conf->{dcc_home} && $cdcc_home) {
    $self->ck_dir($cdcc_home, 'dcc_home', 'cdcc');
  }
  # finally fall back to /var/dcc
  if (!$conf->{dcc_home}) {
    $self->ck_dir($conf->{dcc_home} = '/var/dcc', 'dcc_home', 'default')
  }

  # fall back to $conf->{dcc_home}/libexec or /var/dcc/libexec for dccsight
  if (!$conf->{dcc_libexec}) {
    $self->ck_dir($conf->{dcc_home} . '/libexec', 'dcc_libexec', 'dcc_home');
  }
  if (!$conf->{dcc_libexec}) {
    $self->ck_dir('/var/dcc/libexec', 'dcc_libexec', 'dcc_home');
  }

  # format options for dccifd
  my $opts = ($conf->{dccifd_options} || '') . "\n";
  if ($self->{dcc_version} =~ /\d+\.(\d+)\.(\d+)$/ &&
      ($1 < 3 || ($1 == 3 && $2 < 123))) {
    if ($1 < 3 || ($1 == 3 && $2 < 50)) {
      info("dcc: DCC version $self->{dcc_version} is years old, ".
           "obsolete, and likely to cause problems.  ".
           "See http://www.dcc-servers.net/dcc/old-versions.html");
    }
    $self->{dccifd_lookup_options} = "header " . $opts;
    $self->{dccifd_report_options} = "header spam " . $opts;
  } else {
    # dccifd after version 1.2.123 understands "cksums" and "no-grey"
    $self->{dccifd_lookup_options} = "cksums grey-off " . $opts;
    $self->{dccifd_report_options} = "header spam grey-off " . $opts;
  }
}

sub dcc_pgm_path {
  my ($self, $pgm) = @_;
  my $pgmpath;
  my $conf = $self->{main}->{conf};

  $pgmpath = $conf->{dcc_path};
  if (defined $pgmpath && $pgmpath ne '') {
    # accept explicit setting for dccproc
    return $pgmpath if $pgm eq 'dccproc';
    # try adapting it for cdcc and everything else
    if ($pgmpath =~ s{[^/]+\z}{$pgm}s) {
      $pgmpath = untaint_file_path($pgmpath);
      if (-x $pgmpath) {
        dbg("dcc: dcc_pgm_path, found %s in dcc_path: %s", $pgm,$pgmpath);
        return $pgmpath;
      }
    }
  }

  $pgmpath = Mail::SpamAssassin::Util::find_executable_in_env_path($pgm);
  if (defined $pgmpath) {
    dbg("dcc: dcc_pgm_path, found %s in env.path: %s", $pgm,$pgmpath);
    return $pgmpath;
  }

  # try dcc_home/bin, dcc_libexec, and some desperate last attempts
  foreach my $dir (!defined $conf->{dcc_home} ? () : $conf->{dcc_home}.'/bin',
                   $conf->{dcc_libexec},
                   '/usr/local/bin', '/usr/local/dcc', '/var/dcc') {
    next unless defined $dir;
    $pgmpath = $dir . '/' . $pgm;
    if (-x $pgmpath) {
      dbg("dcc: dcc_pgm_path, found %s in %s: %s", $pgm,$dir,$pgmpath);
      return $pgmpath;
    }
  }

  return;
}

sub is_dccifd_available {
  my ($self) = @_;
  my $conf = $self->{main}->{conf};

  # dccifd remains available until it breaks
  return $self->{dccifd_available} if $self->{dccifd_available};

  # deal with configured INET or INET6 socket
  if (defined $conf->{dcc_dccifd_host}) {
    dbg("dcc: dccifd is available via socket [%s]:%s",
	$conf->{dcc_dccifd_host}, $conf->{dcc_dccifd_port});
    return ($self->{dccifd_available} = 1);
  }

  # the first time here, compute a default local socket based on DCC home
  # from self->find_dcc_home() called elsewhere
  my $sockpath = $conf->{dcc_dccifd_socket};
  if (!$sockpath) {
      if ($conf->{dcc_dccifd_path_raw}) {
	$sockpath = $conf->{dcc_dccifd_path_raw};
      } else {
	$sockpath = "$conf->{dcc_home}/dccifd";
      }
      $conf->{dcc_dccifd_socket} = $sockpath;
  }

  # check the socket every time because it can appear and disappear
  return ($self->{dccifd_available} = 1) if (-S $sockpath && -w _ && -r _);

  dbg("dcc: dccifd is not available; no r/w socket at %s", $sockpath);
  return ($self->{dccifd_available} = 0);
}

sub is_dccproc_available {
  my ($self) = @_;
  my $conf = $self->{main}->{conf};

  # dccproc remains (un)available so check only once
  return $self->{dccproc_available} if  defined $self->{dccproc_available};

  my $dccproc = $conf->{dcc_path};
  if (!defined $dccproc || $dccproc eq '') {
    $dccproc = $self->dcc_pgm_path('dccproc');
    $conf->{dcc_path} = $dccproc;
    if (!$dccproc || ! -x $dccproc) {
      dbg("dcc: dccproc is not available: no dccproc executable found");
      return ($self->{dccproc_available} = 0);
    }
  }

  dbg("dcc: %s is available", $conf->{dcc_path});
  return ($self->{dccproc_available} = 1);
}

sub dccifd_connect {
  my($self, $tag) = @_;
  my $conf = $self->{main}->{conf};
  my $sockpath = $conf->{dcc_dccifd_socket};
  my $sock;

  if (defined $sockpath) {
    dbg("$tag connecting to local socket $sockpath");
    $sock = IO::Socket::UNIX->new(Type => SOCK_STREAM, Peer => $sockpath);
    info("$tag failed to connect to local socket $sockpath") if !$sock;

  } else {  # must be TCP/IP
    my $host = $conf->{dcc_dccifd_host};
    my $port = $conf->{dcc_dccifd_port};
    dbg("$tag connecting to [%s]:%s using %s",
        $host, $port, $io_socket_module_name);
    $sock = $io_socket_module_name->new(
              Proto => 'tcp', PeerAddr => $host, PeerPort => $port);
    info("$tag failed to connect to [%s]:%s using %s: %s",
         $host, $port, $io_socket_module_name, $!) if !$sock;
  }

  $self->{dccifd_available} = 0  if !$sock;
  return $sock;
}

# check for dccifd every time in case enough uses of dccproc starts dccifd
sub get_dcc_interface {
  my ($self) = @_;
  my $conf = $self->{main}->{conf};

  if (!$conf->{use_dcc}) {
    $self->{dcc_disabled} = 1;
    return;
  }

  $self->find_dcc_home();
  if (!$self->is_dccifd_available() && !$self->is_dccproc_available()) {
    dbg("dcc: dccifd and dccproc are not available");
    $self->{dcc_disabled} = 1;
  }

  $self->{dcc_disabled} = 0;
}

sub dcc_query {
  my ($self, $permsgstatus, $fulltext) = @_;

  $permsgstatus->{dcc_checked} = 1;

  if (!$self->{main}->{conf}->{use_dcc}) {
    dbg("dcc: DCC is not available: use_dcc is 0");
    return;
  }

  # initialize valid tags
  $permsgstatus->{tag_data}->{DCCB} = "";
  $permsgstatus->{tag_data}->{DCCR} = "";
  $permsgstatus->{tag_data}->{DCCREP} = "";

  if ($$fulltext eq '') {
    dbg("dcc: empty message; skipping dcc check");
    return;
  }

  if ($permsgstatus->get('ALL') =~ /^(X-DCC-.*-Metrics:.*)$/m) {
    $permsgstatus->{dcc_raw_x_dcc} = $1;
    # short-circuit if there is already a X-DCC header with value of
    # "bulk" from an upstream DCC check
    # require "bulk" because then at least one body checksum will be "many"
    # and so we know the X-DCC header is not forged by spammers
    return if $permsgstatus->{dcc_raw_x_dcc} =~ / bulk /;
  }

  my $timer = $self->{main}->time_method("check_dcc");

  $self->get_dcc_interface();
  return if $self->{dcc_disabled};

  my $envelope = $permsgstatus->{relays_external}->[0];
  ($permsgstatus->{dcc_raw_x_dcc},
   $permsgstatus->{dcc_cksums}) = $self->ask_dcc("dcc:", $permsgstatus,
						 $fulltext, $envelope);
}

sub check_dcc {
  my ($self, $permsgstatus, $full) = @_;
  my $conf = $self->{main}->{conf};

  $self->dcc_query($permsgstatus, $full)  if !$permsgstatus->{dcc_checked};

  my $x_dcc = $permsgstatus->{dcc_raw_x_dcc};
  return 0  if !defined $x_dcc || $x_dcc eq '';

  if ($x_dcc =~ /^X-DCC-(.*)-Metrics: (.*)$/) {
    $permsgstatus->set_tag('DCCB', $1);
    $permsgstatus->set_tag('DCCR', $2);
  }
  $x_dcc =~ s/many/999999/ig;
  $x_dcc =~ s/ok\d?/0/ig;

  my %count = (body => 0, fuz1 => 0, fuz2 => 0, rep => 0);
  if ($x_dcc =~ /\bBody=(\d+)/) {
    $count{body} = $1+0;
  }
  if ($x_dcc =~ /\bFuz1=(\d+)/) {
    $count{fuz1} = $1+0;
  }
  if ($x_dcc =~ /\bFuz2=(\d+)/) {
    $count{fuz2} = $1+0;
  }
  if ($x_dcc =~ /\brep=(\d+)/) {
    $count{rep}  = $1+0;
  }
  if ($count{body} >= $conf->{dcc_body_max} ||
      $count{fuz1} >= $conf->{dcc_fuz1_max} ||
      $count{fuz2} >= $conf->{dcc_fuz2_max} ||
      $count{rep}  >= $conf->{dcc_rep_percent})
  {
    dbg(sprintf("dcc: listed: BODY=%s/%s FUZ1=%s/%s FUZ2=%s/%s REP=%s/%s",
		map { defined $_ ? $_ : 'undef' } (
		  $count{body}, $conf->{dcc_body_max},
		  $count{fuz1}, $conf->{dcc_fuz1_max},
		  $count{fuz2}, $conf->{dcc_fuz2_max},
		  $count{rep},  $conf->{dcc_rep_percent})
		));
    return 1;
  }
  return 0;
}

sub check_dcc_reputation_range {
  my ($self, $permsgstatus, $fulltext, $min, $max) = @_;

  # this is called several times per message, so parse the X-DCC header once
  my $dcc_rep = $permsgstatus->{dcc_rep};
  if (!defined $dcc_rep) {
    $self->dcc_query($permsgstatus, $fulltext)  if !$permsgstatus->{dcc_checked};
    my $x_dcc = $permsgstatus->{dcc_raw_x_dcc};
    if (defined $x_dcc && $x_dcc =~ /\brep=(\d+)/) {
      $dcc_rep = $1+0;
      $permsgstatus->set_tag('DCCREP', $dcc_rep);
    } else {
      $dcc_rep = -1;
    }
    $permsgstatus->{dcc_rep} = $dcc_rep;
  }

  # no X-DCC header or no reputation in the X-DCC header, perhaps for lack
  # of data in the DCC Reputation server
  return 0 if $dcc_rep < 0;

  # cover the entire range of reputations if not told otherwise
  $min = 0   if !defined $min;
  $max = 100 if !defined $max;

  my $result = $dcc_rep >= $min && $dcc_rep <= $max ? 1 : 0;
  dbg("dcc: dcc_rep %s, min %s, max %s => result=%s",
      $dcc_rep, $min, $max, $result?'YES':'no');
  return $result;
}

# get the X-DCC header line and save the checksums from dccifd or dccproc
sub parse_dcc_response {
  my ($self, $resp) = @_;
  my ($raw_x_dcc, $cksums);

  # The first line is the header we want.  It uses SMTP folded whitespace
  # if it is long.  The folded whitespace is always a single \t.
  chomp($raw_x_dcc = shift @$resp);
  my $v;
  while (($v = shift @$resp) && $v =~ s/^\t(.+)\s*\n/ $1/) {
    $raw_x_dcc .= $v;
  }

  # skip the "reported:" line between the X-DCC header and any checksums
  # remove ':' to avoid a bug in versions 1.3.115 - 1.3.122 in dccsight
  # with the length of "Message-ID:"
  $cksums = '';
  while (($v = shift @$resp) && $v =~ s/^([^:]*):/$1/) {
    $cksums .= $v;
  }

  return ($raw_x_dcc, $cksums);
}

sub ask_dcc {
  my ($self, $tag, $permsgstatus, $fulltext, $envelope) = @_;
  my $conf = $self->{main}->{conf};
  my ($pgm, $err, $sock, $pid, @resp);
  my ($client, $clientname, $helo, $opts);

  $permsgstatus->enter_helper_run_mode();

  my $timeout = $conf->{dcc_timeout};
  my $timer = Mail::SpamAssassin::Timeout->new(
	  { secs => $timeout, deadline => $permsgstatus->{master_deadline} });

  $err = $timer->run_and_catch(sub {
    local $SIG{PIPE} = sub { die "__brokenpipe__ignore__\n" };

    # prefer dccifd to dccproc
    if ($self->{dccifd_available}) {
      $pgm = 'dccifd';

      $sock = $self->dccifd_connect($tag);
      if (!$sock) {
	$self->{dccifd_available} = 0;
	die("dccproc not available") if (!$self->is_dccproc_available());

	# fall back on dccproc if the socket is an orphan from
	# a killed dccifd daemon or some other obvious (no timeout) problem
	dbg("$tag fall back on dccproc");
      }
    }

    if ($self->{dccifd_available}) {

      # send the options and other parameters to the daemon
      $client = $envelope->{ip};
      $clientname = $envelope->{rdns};
      if (!defined $client) {
	$client = '';
      } else {
	$client .= ("\r" . $clientname) if defined $clientname;
      }
      $helo = $envelope->{helo} || '';
      if ($tag ne "dcc:") {
	$opts = $self->{dccifd_report_options}
      } else {
	$opts = $self->{dccifd_lookup_options};
	if (defined $permsgstatus->{dcc_raw_x_dcc}) {
	  # only query if there is an X-DCC header
	  $opts =~ s/grey-off/grey-off query/;
	}
      }

      $sock->print($opts)	   or die "failed write options\n";
      $sock->print($client . "\n") or die "failed write SMTP client\n";
      $sock->print($helo . "\n")   or die "failed write HELO value\n";
      $sock->print("\n")	   or die "failed write sender\n";
      $sock->print("unknown\n\n")  or die "failed write 1 recipient\n";
      $sock->print($$fulltext)     or die "failed write mail message\n";
      $sock->shutdown(1) or die "failed socket shutdown: $!";

      $sock->getline()   or die "failed read status\n";
      $sock->getline()   or die "failed read multistatus\n";

      @resp = $sock->getlines();
      die "failed to read dccifd response\n" if !@resp;

    } else {
      $pgm = 'dccproc';
      # use a temp file -- open2() is unreliable, buffering-wise, under spamd
      # first ensure that we do not hit a stray file from some other filter.
      $permsgstatus->delete_fulltext_tmpfile();
      my $tmpf = $permsgstatus->create_fulltext_tmpfile($fulltext);

      my $path = $conf->{dcc_path};
      $opts = $conf->{dcc_options};
      my @opts = !defined $opts ? () : split(' ',$opts);
      untaint_var(\@opts);
      unshift(@opts, '-w', 'whiteclnt');
      $client = $envelope->{ip};
      if ($client) {
	unshift(@opts, '-a', untaint_var($client));
      } else {
	# get external relay IP address from Received: header if not available
	unshift(@opts, '-R');
      }
      if ($tag eq "dcc:") {
	# query instead of report if there is an X-DCC header from upstream
	unshift(@opts, '-Q') if defined $permsgstatus->{dcc_raw_x_dcc};
      } else {
	# learn or report spam
	unshift(@opts, '-t', 'many');
      }
      if ($conf->{dcc_home}) {
        # set home directory explicitly
        unshift(@opts, '-h', $conf->{dcc_home});
      };

      defined $path  or die "no dcc_path found\n";
      dbg("$tag opening pipe to " .
	  join(' ', $path, "-C", "-x", "0", @opts, "<$tmpf"));

      $pid = Mail::SpamAssassin::Util::helper_app_pipe_open(*DCC,
		$tmpf, 1, $path, "-C", "-x", "0", @opts);
      $pid or die "DCC: $!\n";

      # read+split avoids a Perl I/O bug (Bug 5985)
      my($inbuf,$nread,$resp); $resp = '';
      while ( $nread=read(DCC,$inbuf,8192) ) { $resp .= $inbuf }
      defined $nread  or die "error reading from pipe: $!";
      @resp = split(/^/m, $resp, -1);  undef $resp;

      my $errno = 0;  close DCC or $errno = $!;
      proc_status_ok($?,$errno)
	  or info("$tag [%s] finished: %s", $pid, exit_status_str($?,$errno));

      die "failed to read X-DCC header from dccproc\n" if !@resp;
    }
  });

  if (defined $pgm && $pgm eq 'dccproc') {
    if (defined(fileno(*DCC))) {	# still open
      if ($pid) {
	if (kill('TERM',$pid)) {
	  dbg("$tag killed stale dccproc process [$pid]")
	} else {
	  dbg("$tag killing dccproc process [$pid] failed: $!")
	}
      }
      my $errno = 0;  close(DCC) or $errno = $!;
      proc_status_ok($?,$errno) or info("$tag [%s] dccproc terminated: %s",
					$pid, exit_status_str($?,$errno));
    }
  }

  $permsgstatus->leave_helper_run_mode();

  if ($timer->timed_out()) {
    dbg("$tag %s timed out after %d seconds", $pgm||'', $timeout);
    return (undef, undef);
  }

  if ($err) {
    chomp $err;
    info("$tag %s failed: %s", $pgm||'', $err);
    return (undef, undef);
  }

  my ($raw_x_dcc, $cksums) = $self->parse_dcc_response(\@resp);
  if (!defined $raw_x_dcc || $raw_x_dcc !~ /^X-DCC/) {
    info("$tag instead of X-DCC header, $pgm returned '$raw_x_dcc'");
    return (undef, undef);
  }
  dbg("$tag $pgm responded with '$raw_x_dcc'");
  return ($raw_x_dcc, $cksums);
}

# tell DCC server that the message is spam according to SpamAssassin
sub check_post_learn {
  my ($self, $options) = @_;

  # learn only if allowed
  return if $self->{learn_disabled};
  my $conf = $self->{main}->{conf};
  if (!$conf->{use_dcc}) {
    $self->{learn_disabled} = 1;
    return;
  }
  my $learn_score = $conf->{dcc_learn_score};
  if (!defined $learn_score || $learn_score eq '') {
    dbg("dcc: DCC learning not enabled by dcc_learn_score");
    $self->{learn_disabled} = 1;
    return;
  }

  # and if SpamAssassin concluded that the message is spam
  # worse than our threshold
  my $permsgstatus = $options->{permsgstatus};
  if ($permsgstatus->is_spam()) {
    my $score = $permsgstatus->get_score();
    my $required_score = $permsgstatus->get_required_score();
    if ($score < $required_score + $learn_score) {
      dbg("dcc: score=%d required_score=%d dcc_learn_score=%d",
	  $score, $required_score, $learn_score);
      return;
    }
  }

  # and if we checked the message
  return if (!defined $permsgstatus->{dcc_raw_x_dcc});

  # and if the DCC server thinks it was not spam
  if ($permsgstatus->{dcc_raw_x_dcc} !~ /\b(Body|Fuz1|Fuz2)=\d/) {
    dbg("dcc: already known as spam; no need to learn");
    return;
  }

  # dccsight is faster than dccifd or dccproc if we have checksums,
  #   which we do not have with dccifd before 1.3.123
  my $old_cksums = $permsgstatus->{dcc_cksums};
  return if ($old_cksums && $self->dccsight_learn($permsgstatus, $old_cksums));

  # Fall back on dccifd or dccproc without saved checksums or dccsight.
  # get_dcc_interface() was called when the message was checked

  # is getting the full text this way kosher?  Is get_pristine() public?
  my $fulltext = $permsgstatus->{msg}->get_pristine();
  my $envelope = $permsgstatus->{relays_external}->[0];
  my ($raw_x_dcc, $cksums) = $self->ask_dcc("dcc: learn:", $permsgstatus,
					    \$fulltext, $envelope);
  dbg("dcc: learned as spam") if defined $raw_x_dcc;
}

sub dccsight_learn {
  my ($self, $permsgstatus, $old_cksums) = @_;
  my ($raw_x_dcc, $new_cksums);

  return 0 if !$old_cksums;

  my $dccsight = $self->dcc_pgm_path('dccsight');
  if (!$dccsight) {
    info("dcc: cannot find dccsight") if $dccsight eq '';
    return 0;
  }

  $permsgstatus->enter_helper_run_mode();

  # use a temp file here -- open2() is unreliable, buffering-wise, under spamd
  # ensure that we do not hit a stray file from some other filter.
  $permsgstatus->delete_fulltext_tmpfile();
  my $tmpf = $permsgstatus->create_fulltext_tmpfile(\$old_cksums);
  my $pid;

  my $timeout = $self->{main}->{conf}->{dcc_timeout};
  my $timer = Mail::SpamAssassin::Timeout->new(
	   { secs => $timeout, deadline => $permsgstatus->{master_deadline} });
  my $err = $timer->run_and_catch(sub {
    local $SIG{PIPE} = sub { die "__brokenpipe__ignore__\n" };

    dbg("dcc: opening pipe to %s",
	join(' ', $dccsight, "-t", "many", "<$tmpf"));

    $pid = Mail::SpamAssassin::Util::helper_app_pipe_open(*DCC,
	    $tmpf, 1, $dccsight, "-t", "many");
    $pid or die "$!\n";

    # read+split avoids a Perl I/O bug (Bug 5985)
    my($inbuf,$nread,$resp); $resp = '';
    while ( $nread=read(DCC,$inbuf,8192) ) { $resp .= $inbuf }
    defined $nread  or die "error reading from pipe: $!";
    my @resp = split(/^/m, $resp, -1);  undef $resp;

    my $errno = 0;  close DCC or $errno = $!;
    proc_status_ok($?,$errno)
	  or info("dcc: [%s] finished: %s", $pid, exit_status_str($?,$errno));

    die "dcc: failed to read learning response\n" if !@resp;

    ($raw_x_dcc, $new_cksums) = $self->parse_dcc_response(\@resp);
  });

  if (defined(fileno(*DCC))) {	  # still open
    if ($pid) {
      if (kill('TERM',$pid)) {
	dbg("dcc: killed stale dccsight process [$pid]")
      } else {
	dbg("dcc: killing stale dccsight process [$pid] failed: $!") }
    }
    my $errno = 0;  close(DCC) or $errno = $!;
    proc_status_ok($?,$errno) or info("dcc: dccsight [%s] terminated: %s",
				      $pid, exit_status_str($?,$errno));
  }
  $permsgstatus->delete_fulltext_tmpfile();
  $permsgstatus->leave_helper_run_mode();

  if ($timer->timed_out()) {
    dbg("dcc: dccsight timed out after $timeout seconds");
    return 0;
  }

  if ($err) {
    chomp $err;
    info("dcc: dccsight failed: $err\n");
    return 0;
  }

  if ($raw_x_dcc) {
    dbg("dcc: learned response: %s", $raw_x_dcc);
    return 1;
  }

  return 0;
}

sub plugin_report {
  my ($self, $options) = @_;

  return if $options->{report}->{options}->{dont_report_to_dcc};
  $self->get_dcc_interface();
  return if $self->{dcc_disabled};

  # get the metadata from the message so we can report the external relay
  $options->{msg}->extract_message_metadata($options->{report}->{main});
  my $envelope = $options->{msg}->{metadata}->{relays_external}->[0];
  my ($raw_x_dcc, $cksums) = $self->ask_dcc("reporter:", $options->{report},
					    $options->{text}, $envelope);

  if (defined $raw_x_dcc) {
    $options->{report}->{report_available} = 1;
    info("reporter: spam reported to DCC");
    $options->{report}->{report_return} = 1;
  } else {
    info("reporter: could not report spam to DCC");
  }
}

1;