amavisd-new 640 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162416341644165416641674168416941704171417241734174417541764177417841794180418141824183418441854186418741884189419041914192419341944195419641974198419942004201420242034204420542064207420842094210421142124213421442154216421742184219422042214222422342244225422642274228422942304231423242334234423542364237423842394240424142424243424442454246424742484249425042514252425342544255425642574258425942604261426242634264426542664267426842694270427142724273427442754276427742784279428042814282428342844285428642874288428942904291429242934294429542964297429842994300430143024303430443054306430743084309431043114312431343144315431643174318431943204321432243234324432543264327432843294330433143324333433443354336433743384339434043414342434343444345434643474348434943504351435243534354435543564357435843594360436143624363436443654366436743684369437043714372437343744375437643774378437943804381438243834384438543864387438843894390439143924393439443954396439743984399440044014402440344044405440644074408440944104411441244134414441544164417441844194420442144224423442444254426442744284429443044314432443344344435443644374438443944404441444244434444444544464447444844494450445144524453445444554456445744584459446044614462446344644465446644674468446944704471447244734474447544764477447844794480448144824483448444854486448744884489449044914492449344944495449644974498449945004501450245034504450545064507450845094510451145124513451445154516451745184519452045214522452345244525452645274528452945304531453245334534453545364537453845394540454145424543454445454546454745484549455045514552455345544555455645574558455945604561456245634564456545664567456845694570457145724573457445754576457745784579458045814582458345844585458645874588458945904591459245934594459545964597459845994600460146024603460446054606460746084609461046114612461346144615461646174618461946204621462246234624462546264627462846294630463146324633463446354636463746384639464046414642464346444645464646474648464946504651465246534654465546564657465846594660466146624663466446654666466746684669467046714672467346744675467646774678467946804681468246834684468546864687468846894690469146924693469446954696469746984699470047014702470347044705470647074708470947104711471247134714471547164717471847194720472147224723472447254726472747284729473047314732473347344735473647374738473947404741474247434744474547464747474847494750475147524753475447554756475747584759476047614762476347644765476647674768476947704771477247734774477547764777477847794780478147824783478447854786478747884789479047914792479347944795479647974798479948004801480248034804480548064807480848094810481148124813481448154816481748184819482048214822482348244825482648274828482948304831483248334834483548364837483848394840484148424843484448454846484748484849485048514852485348544855485648574858485948604861486248634864486548664867486848694870487148724873487448754876487748784879488048814882488348844885488648874888488948904891489248934894489548964897489848994900490149024903490449054906490749084909491049114912491349144915491649174918491949204921492249234924492549264927492849294930493149324933493449354936493749384939494049414942494349444945494649474948494949504951495249534954495549564957495849594960496149624963496449654966496749684969497049714972497349744975497649774978497949804981498249834984498549864987498849894990499149924993499449954996499749984999500050015002500350045005500650075008500950105011501250135014501550165017501850195020502150225023502450255026502750285029503050315032503350345035503650375038503950405041504250435044504550465047504850495050505150525053505450555056505750585059506050615062506350645065506650675068506950705071507250735074507550765077507850795080508150825083508450855086508750885089509050915092509350945095509650975098509951005101510251035104510551065107510851095110511151125113511451155116511751185119512051215122512351245125512651275128512951305131513251335134513551365137513851395140514151425143514451455146514751485149515051515152515351545155515651575158515951605161516251635164516551665167516851695170517151725173517451755176517751785179518051815182518351845185518651875188518951905191519251935194519551965197519851995200520152025203520452055206520752085209521052115212521352145215521652175218521952205221522252235224522552265227522852295230523152325233523452355236523752385239524052415242524352445245524652475248524952505251525252535254525552565257525852595260526152625263526452655266526752685269527052715272527352745275527652775278527952805281528252835284528552865287528852895290529152925293529452955296529752985299530053015302530353045305530653075308530953105311531253135314531553165317531853195320532153225323532453255326532753285329533053315332533353345335533653375338533953405341534253435344534553465347534853495350535153525353535453555356535753585359536053615362536353645365536653675368536953705371537253735374537553765377537853795380538153825383538453855386538753885389539053915392539353945395539653975398539954005401540254035404540554065407540854095410541154125413541454155416541754185419542054215422542354245425542654275428542954305431543254335434543554365437543854395440544154425443544454455446544754485449545054515452545354545455545654575458545954605461546254635464546554665467546854695470547154725473547454755476547754785479548054815482548354845485548654875488548954905491549254935494549554965497549854995500550155025503550455055506550755085509551055115512551355145515551655175518551955205521552255235524552555265527552855295530553155325533553455355536553755385539554055415542554355445545554655475548554955505551555255535554555555565557555855595560556155625563556455655566556755685569557055715572557355745575557655775578557955805581558255835584558555865587558855895590559155925593559455955596559755985599560056015602560356045605560656075608560956105611561256135614561556165617561856195620562156225623562456255626562756285629563056315632563356345635563656375638563956405641564256435644564556465647564856495650565156525653565456555656565756585659566056615662566356645665566656675668566956705671567256735674567556765677567856795680568156825683568456855686568756885689569056915692569356945695569656975698569957005701570257035704570557065707570857095710571157125713571457155716571757185719572057215722572357245725572657275728572957305731573257335734573557365737573857395740574157425743574457455746574757485749575057515752575357545755575657575758575957605761576257635764576557665767576857695770577157725773577457755776577757785779578057815782578357845785578657875788578957905791579257935794579557965797579857995800580158025803580458055806580758085809581058115812581358145815581658175818581958205821582258235824582558265827582858295830583158325833583458355836583758385839584058415842584358445845584658475848584958505851585258535854585558565857585858595860586158625863586458655866586758685869587058715872587358745875587658775878587958805881588258835884588558865887588858895890589158925893589458955896589758985899590059015902590359045905590659075908590959105911591259135914591559165917591859195920592159225923592459255926592759285929593059315932593359345935593659375938593959405941594259435944594559465947594859495950595159525953595459555956595759585959596059615962596359645965596659675968596959705971597259735974597559765977597859795980598159825983598459855986598759885989599059915992599359945995599659975998599960006001600260036004600560066007600860096010601160126013601460156016601760186019602060216022602360246025602660276028602960306031603260336034603560366037603860396040604160426043604460456046604760486049605060516052605360546055605660576058605960606061606260636064606560666067606860696070607160726073607460756076607760786079608060816082608360846085608660876088608960906091609260936094609560966097609860996100610161026103610461056106610761086109611061116112611361146115611661176118611961206121612261236124612561266127612861296130613161326133613461356136613761386139614061416142614361446145614661476148614961506151615261536154615561566157615861596160616161626163616461656166616761686169617061716172617361746175617661776178617961806181618261836184618561866187618861896190619161926193619461956196619761986199620062016202620362046205620662076208620962106211621262136214621562166217621862196220622162226223622462256226622762286229623062316232623362346235623662376238623962406241624262436244624562466247624862496250625162526253625462556256625762586259626062616262626362646265626662676268626962706271627262736274627562766277627862796280628162826283628462856286628762886289629062916292629362946295629662976298629963006301630263036304630563066307630863096310631163126313631463156316631763186319632063216322632363246325632663276328632963306331633263336334633563366337633863396340634163426343634463456346634763486349635063516352635363546355635663576358635963606361636263636364636563666367636863696370637163726373637463756376637763786379638063816382638363846385638663876388638963906391639263936394639563966397639863996400640164026403640464056406640764086409641064116412641364146415641664176418641964206421642264236424642564266427642864296430643164326433643464356436643764386439644064416442644364446445644664476448644964506451645264536454645564566457645864596460646164626463646464656466646764686469647064716472647364746475647664776478647964806481648264836484648564866487648864896490649164926493649464956496649764986499650065016502650365046505650665076508650965106511651265136514651565166517651865196520652165226523652465256526652765286529653065316532653365346535653665376538653965406541654265436544654565466547654865496550655165526553655465556556655765586559656065616562656365646565656665676568656965706571657265736574657565766577657865796580658165826583658465856586658765886589659065916592659365946595659665976598659966006601660266036604660566066607660866096610661166126613661466156616661766186619662066216622662366246625662666276628662966306631663266336634663566366637663866396640664166426643664466456646664766486649665066516652665366546655665666576658665966606661666266636664666566666667666866696670667166726673667466756676667766786679668066816682668366846685668666876688668966906691669266936694669566966697669866996700670167026703670467056706670767086709671067116712671367146715671667176718671967206721672267236724672567266727672867296730673167326733673467356736673767386739674067416742674367446745674667476748674967506751675267536754675567566757675867596760676167626763676467656766676767686769677067716772677367746775677667776778677967806781678267836784678567866787678867896790679167926793679467956796679767986799680068016802680368046805680668076808680968106811681268136814681568166817681868196820682168226823682468256826682768286829683068316832683368346835683668376838683968406841684268436844684568466847684868496850685168526853685468556856685768586859686068616862686368646865686668676868686968706871687268736874687568766877687868796880688168826883688468856886688768886889689068916892689368946895689668976898689969006901690269036904690569066907690869096910691169126913691469156916691769186919692069216922692369246925692669276928692969306931693269336934693569366937693869396940694169426943694469456946694769486949695069516952695369546955695669576958695969606961696269636964696569666967696869696970697169726973697469756976697769786979698069816982698369846985698669876988698969906991699269936994699569966997699869997000700170027003700470057006700770087009701070117012701370147015701670177018701970207021702270237024702570267027702870297030703170327033703470357036703770387039704070417042704370447045704670477048704970507051705270537054705570567057705870597060706170627063706470657066706770687069707070717072707370747075707670777078707970807081708270837084708570867087708870897090709170927093709470957096709770987099710071017102710371047105710671077108710971107111711271137114711571167117711871197120712171227123712471257126712771287129713071317132713371347135713671377138713971407141714271437144714571467147714871497150715171527153715471557156715771587159716071617162716371647165716671677168716971707171717271737174717571767177717871797180718171827183718471857186718771887189719071917192719371947195719671977198719972007201720272037204720572067207720872097210721172127213721472157216721772187219722072217222722372247225722672277228722972307231723272337234723572367237723872397240724172427243724472457246724772487249725072517252725372547255725672577258725972607261726272637264726572667267726872697270727172727273727472757276727772787279728072817282728372847285728672877288728972907291729272937294729572967297729872997300730173027303730473057306730773087309731073117312731373147315731673177318731973207321732273237324732573267327732873297330733173327333733473357336733773387339734073417342734373447345734673477348734973507351735273537354735573567357735873597360736173627363736473657366736773687369737073717372737373747375737673777378737973807381738273837384738573867387738873897390739173927393739473957396739773987399740074017402740374047405740674077408740974107411741274137414741574167417741874197420742174227423742474257426742774287429743074317432743374347435743674377438743974407441744274437444744574467447744874497450745174527453745474557456745774587459746074617462746374647465746674677468746974707471747274737474747574767477747874797480748174827483748474857486748774887489749074917492749374947495749674977498749975007501750275037504750575067507750875097510751175127513751475157516751775187519752075217522752375247525752675277528752975307531753275337534753575367537753875397540754175427543754475457546754775487549755075517552755375547555755675577558755975607561756275637564756575667567756875697570757175727573757475757576757775787579758075817582758375847585758675877588758975907591759275937594759575967597759875997600760176027603760476057606760776087609761076117612761376147615761676177618761976207621762276237624762576267627762876297630763176327633763476357636763776387639764076417642764376447645764676477648764976507651765276537654765576567657765876597660766176627663766476657666766776687669767076717672767376747675767676777678767976807681768276837684768576867687768876897690769176927693769476957696769776987699770077017702770377047705770677077708770977107711771277137714771577167717771877197720772177227723772477257726772777287729773077317732773377347735773677377738773977407741774277437744774577467747774877497750775177527753775477557756775777587759776077617762776377647765776677677768776977707771777277737774777577767777777877797780778177827783778477857786778777887789779077917792779377947795779677977798779978007801780278037804780578067807780878097810781178127813781478157816781778187819782078217822782378247825782678277828782978307831783278337834783578367837783878397840784178427843784478457846784778487849785078517852785378547855785678577858785978607861786278637864786578667867786878697870787178727873787478757876787778787879788078817882788378847885788678877888788978907891789278937894789578967897789878997900790179027903790479057906790779087909791079117912791379147915791679177918791979207921792279237924792579267927792879297930793179327933793479357936793779387939794079417942794379447945794679477948794979507951795279537954795579567957795879597960796179627963796479657966796779687969797079717972797379747975797679777978797979807981798279837984798579867987798879897990799179927993799479957996799779987999800080018002800380048005800680078008800980108011801280138014801580168017801880198020802180228023802480258026802780288029803080318032803380348035803680378038803980408041804280438044804580468047804880498050805180528053805480558056805780588059806080618062806380648065806680678068806980708071807280738074807580768077807880798080808180828083808480858086808780888089809080918092809380948095809680978098809981008101810281038104810581068107810881098110811181128113811481158116811781188119812081218122812381248125812681278128812981308131813281338134813581368137813881398140814181428143814481458146814781488149815081518152815381548155815681578158815981608161816281638164816581668167816881698170817181728173817481758176817781788179818081818182818381848185818681878188818981908191819281938194819581968197819881998200820182028203820482058206820782088209821082118212821382148215821682178218821982208221822282238224822582268227822882298230823182328233823482358236823782388239824082418242824382448245824682478248824982508251825282538254825582568257825882598260826182628263826482658266826782688269827082718272827382748275827682778278827982808281828282838284828582868287828882898290829182928293829482958296829782988299830083018302830383048305830683078308830983108311831283138314831583168317831883198320832183228323832483258326832783288329833083318332833383348335833683378338833983408341834283438344834583468347834883498350835183528353835483558356835783588359836083618362836383648365836683678368836983708371837283738374837583768377837883798380838183828383838483858386838783888389839083918392839383948395839683978398839984008401840284038404840584068407840884098410841184128413841484158416841784188419842084218422842384248425842684278428842984308431843284338434843584368437843884398440844184428443844484458446844784488449845084518452845384548455845684578458845984608461846284638464846584668467846884698470847184728473847484758476847784788479848084818482848384848485848684878488848984908491849284938494849584968497849884998500850185028503850485058506850785088509851085118512851385148515851685178518851985208521852285238524852585268527852885298530853185328533853485358536853785388539854085418542854385448545854685478548854985508551855285538554855585568557855885598560856185628563856485658566856785688569857085718572857385748575857685778578857985808581858285838584858585868587858885898590859185928593859485958596859785988599860086018602860386048605860686078608860986108611861286138614861586168617861886198620862186228623862486258626862786288629863086318632863386348635863686378638863986408641864286438644864586468647864886498650865186528653865486558656865786588659866086618662866386648665866686678668866986708671867286738674867586768677867886798680868186828683868486858686868786888689869086918692869386948695869686978698869987008701870287038704870587068707870887098710871187128713871487158716871787188719872087218722872387248725872687278728872987308731873287338734873587368737873887398740874187428743874487458746874787488749875087518752875387548755875687578758875987608761876287638764876587668767876887698770877187728773877487758776877787788779878087818782878387848785878687878788878987908791879287938794879587968797879887998800880188028803880488058806880788088809881088118812881388148815881688178818881988208821882288238824882588268827882888298830883188328833883488358836883788388839884088418842884388448845884688478848884988508851885288538854885588568857885888598860886188628863886488658866886788688869887088718872887388748875887688778878887988808881888288838884888588868887888888898890889188928893889488958896889788988899890089018902890389048905890689078908890989108911891289138914891589168917891889198920892189228923892489258926892789288929893089318932893389348935893689378938893989408941894289438944894589468947894889498950895189528953895489558956895789588959896089618962896389648965896689678968896989708971897289738974897589768977897889798980898189828983898489858986898789888989899089918992899389948995899689978998899990009001900290039004900590069007900890099010901190129013901490159016901790189019902090219022902390249025902690279028902990309031903290339034903590369037903890399040904190429043904490459046904790489049905090519052905390549055905690579058905990609061906290639064906590669067906890699070907190729073907490759076907790789079908090819082908390849085908690879088908990909091909290939094909590969097909890999100910191029103910491059106910791089109911091119112911391149115911691179118911991209121912291239124912591269127912891299130913191329133913491359136913791389139914091419142914391449145914691479148914991509151915291539154915591569157915891599160916191629163916491659166916791689169917091719172917391749175917691779178917991809181918291839184918591869187918891899190919191929193919491959196919791989199920092019202920392049205920692079208920992109211921292139214921592169217921892199220922192229223922492259226922792289229923092319232923392349235923692379238923992409241924292439244924592469247924892499250925192529253925492559256925792589259926092619262926392649265926692679268926992709271927292739274927592769277927892799280928192829283928492859286928792889289929092919292929392949295929692979298929993009301930293039304930593069307930893099310931193129313931493159316931793189319932093219322932393249325932693279328932993309331933293339334933593369337933893399340934193429343934493459346934793489349935093519352935393549355935693579358935993609361936293639364936593669367936893699370937193729373937493759376937793789379938093819382938393849385938693879388938993909391939293939394939593969397939893999400940194029403940494059406940794089409941094119412941394149415941694179418941994209421942294239424942594269427942894299430943194329433943494359436943794389439944094419442944394449445944694479448944994509451945294539454945594569457945894599460946194629463946494659466946794689469947094719472947394749475947694779478947994809481948294839484948594869487948894899490949194929493949494959496949794989499950095019502950395049505950695079508950995109511951295139514951595169517951895199520952195229523952495259526952795289529953095319532953395349535953695379538953995409541954295439544954595469547954895499550955195529553955495559556955795589559956095619562956395649565956695679568956995709571957295739574957595769577957895799580958195829583958495859586958795889589959095919592959395949595959695979598959996009601960296039604960596069607960896099610961196129613961496159616961796189619962096219622962396249625962696279628962996309631963296339634963596369637963896399640964196429643964496459646964796489649965096519652965396549655965696579658965996609661966296639664966596669667966896699670967196729673967496759676967796789679968096819682968396849685968696879688968996909691969296939694969596969697969896999700970197029703970497059706970797089709971097119712971397149715971697179718971997209721972297239724972597269727972897299730973197329733973497359736973797389739974097419742974397449745974697479748974997509751975297539754975597569757975897599760976197629763976497659766976797689769977097719772977397749775977697779778977997809781978297839784978597869787978897899790979197929793979497959796979797989799980098019802980398049805980698079808980998109811981298139814981598169817981898199820982198229823982498259826982798289829983098319832983398349835983698379838983998409841984298439844984598469847984898499850985198529853985498559856985798589859986098619862986398649865986698679868986998709871987298739874987598769877987898799880988198829883988498859886988798889889989098919892989398949895989698979898989999009901990299039904990599069907990899099910991199129913991499159916991799189919992099219922992399249925992699279928992999309931993299339934993599369937993899399940994199429943994499459946994799489949995099519952995399549955995699579958995999609961996299639964996599669967996899699970997199729973997499759976997799789979998099819982998399849985998699879988998999909991999299939994999599969997999899991000010001100021000310004100051000610007100081000910010100111001210013100141001510016100171001810019100201002110022100231002410025100261002710028100291003010031100321003310034100351003610037100381003910040100411004210043100441004510046100471004810049100501005110052100531005410055100561005710058100591006010061100621006310064100651006610067100681006910070100711007210073100741007510076100771007810079100801008110082100831008410085100861008710088100891009010091100921009310094100951009610097100981009910100101011010210103101041010510106101071010810109101101011110112101131011410115101161011710118101191012010121101221012310124101251012610127101281012910130101311013210133101341013510136101371013810139101401014110142101431014410145101461014710148101491015010151101521015310154101551015610157101581015910160101611016210163101641016510166101671016810169101701017110172101731017410175101761017710178101791018010181101821018310184101851018610187101881018910190101911019210193101941019510196101971019810199102001020110202102031020410205102061020710208102091021010211102121021310214102151021610217102181021910220102211022210223102241022510226102271022810229102301023110232102331023410235102361023710238102391024010241102421024310244102451024610247102481024910250102511025210253102541025510256102571025810259102601026110262102631026410265102661026710268102691027010271102721027310274102751027610277102781027910280102811028210283102841028510286102871028810289102901029110292102931029410295102961029710298102991030010301103021030310304103051030610307103081030910310103111031210313103141031510316103171031810319103201032110322103231032410325103261032710328103291033010331103321033310334103351033610337103381033910340103411034210343103441034510346103471034810349103501035110352103531035410355103561035710358103591036010361103621036310364103651036610367103681036910370103711037210373103741037510376103771037810379103801038110382103831038410385103861038710388103891039010391103921039310394103951039610397103981039910400104011040210403104041040510406104071040810409104101041110412104131041410415104161041710418104191042010421104221042310424104251042610427104281042910430104311043210433104341043510436104371043810439104401044110442104431044410445104461044710448104491045010451104521045310454104551045610457104581045910460104611046210463104641046510466104671046810469104701047110472104731047410475104761047710478104791048010481104821048310484104851048610487104881048910490104911049210493104941049510496104971049810499105001050110502105031050410505105061050710508105091051010511105121051310514105151051610517105181051910520105211052210523105241052510526105271052810529105301053110532105331053410535105361053710538105391054010541105421054310544105451054610547105481054910550105511055210553105541055510556105571055810559105601056110562105631056410565105661056710568105691057010571105721057310574105751057610577105781057910580105811058210583105841058510586105871058810589105901059110592105931059410595105961059710598105991060010601106021060310604106051060610607106081060910610106111061210613106141061510616106171061810619106201062110622106231062410625106261062710628106291063010631106321063310634106351063610637106381063910640106411064210643106441064510646106471064810649106501065110652106531065410655106561065710658106591066010661106621066310664106651066610667106681066910670106711067210673106741067510676106771067810679106801068110682106831068410685106861068710688106891069010691106921069310694106951069610697106981069910700107011070210703107041070510706107071070810709107101071110712107131071410715107161071710718107191072010721107221072310724107251072610727107281072910730107311073210733107341073510736107371073810739107401074110742107431074410745107461074710748107491075010751107521075310754107551075610757107581075910760107611076210763107641076510766107671076810769107701077110772107731077410775107761077710778107791078010781107821078310784107851078610787107881078910790107911079210793107941079510796107971079810799108001080110802108031080410805108061080710808108091081010811108121081310814108151081610817108181081910820108211082210823108241082510826108271082810829108301083110832108331083410835108361083710838108391084010841108421084310844108451084610847108481084910850108511085210853108541085510856108571085810859108601086110862108631086410865108661086710868108691087010871108721087310874108751087610877108781087910880108811088210883108841088510886108871088810889108901089110892108931089410895108961089710898108991090010901109021090310904109051090610907109081090910910109111091210913109141091510916109171091810919109201092110922109231092410925109261092710928109291093010931109321093310934109351093610937109381093910940109411094210943109441094510946109471094810949109501095110952109531095410955109561095710958109591096010961109621096310964109651096610967109681096910970109711097210973109741097510976109771097810979109801098110982109831098410985109861098710988109891099010991109921099310994109951099610997109981099911000110011100211003110041100511006110071100811009110101101111012110131101411015110161101711018110191102011021110221102311024110251102611027110281102911030110311103211033110341103511036110371103811039110401104111042110431104411045110461104711048110491105011051110521105311054110551105611057110581105911060110611106211063110641106511066110671106811069110701107111072110731107411075110761107711078110791108011081110821108311084110851108611087110881108911090110911109211093110941109511096110971109811099111001110111102111031110411105111061110711108111091111011111111121111311114111151111611117111181111911120111211112211123111241112511126111271112811129111301113111132111331113411135111361113711138111391114011141111421114311144111451114611147111481114911150111511115211153111541115511156111571115811159111601116111162111631116411165111661116711168111691117011171111721117311174111751117611177111781117911180111811118211183111841118511186111871118811189111901119111192111931119411195111961119711198111991120011201112021120311204112051120611207112081120911210112111121211213112141121511216112171121811219112201122111222112231122411225112261122711228112291123011231112321123311234112351123611237112381123911240112411124211243112441124511246112471124811249112501125111252112531125411255112561125711258112591126011261112621126311264112651126611267112681126911270112711127211273112741127511276112771127811279112801128111282112831128411285112861128711288112891129011291112921129311294112951129611297112981129911300113011130211303113041130511306113071130811309113101131111312113131131411315113161131711318113191132011321113221132311324113251132611327113281132911330113311133211333113341133511336113371133811339113401134111342113431134411345113461134711348113491135011351113521135311354113551135611357113581135911360113611136211363113641136511366113671136811369113701137111372113731137411375113761137711378113791138011381113821138311384113851138611387113881138911390113911139211393113941139511396113971139811399114001140111402114031140411405114061140711408114091141011411114121141311414114151141611417114181141911420114211142211423114241142511426114271142811429114301143111432114331143411435114361143711438114391144011441114421144311444114451144611447114481144911450114511145211453114541145511456114571145811459114601146111462114631146411465114661146711468114691147011471114721147311474114751147611477114781147911480114811148211483114841148511486114871148811489114901149111492114931149411495114961149711498114991150011501115021150311504115051150611507115081150911510115111151211513115141151511516115171151811519115201152111522115231152411525115261152711528115291153011531115321153311534115351153611537115381153911540115411154211543115441154511546115471154811549115501155111552115531155411555115561155711558115591156011561115621156311564115651156611567115681156911570115711157211573115741157511576115771157811579115801158111582115831158411585115861158711588115891159011591115921159311594115951159611597115981159911600116011160211603116041160511606116071160811609116101161111612116131161411615116161161711618116191162011621116221162311624116251162611627116281162911630116311163211633116341163511636116371163811639116401164111642116431164411645116461164711648116491165011651116521165311654116551165611657116581165911660116611166211663116641166511666116671166811669116701167111672116731167411675116761167711678116791168011681116821168311684116851168611687116881168911690116911169211693116941169511696116971169811699117001170111702117031170411705117061170711708117091171011711117121171311714117151171611717117181171911720117211172211723117241172511726117271172811729117301173111732117331173411735117361173711738117391174011741117421174311744117451174611747117481174911750117511175211753117541175511756117571175811759117601176111762117631176411765117661176711768117691177011771117721177311774117751177611777117781177911780117811178211783117841178511786117871178811789117901179111792117931179411795117961179711798117991180011801118021180311804118051180611807118081180911810118111181211813118141181511816118171181811819118201182111822118231182411825118261182711828118291183011831118321183311834118351183611837118381183911840118411184211843118441184511846118471184811849118501185111852118531185411855118561185711858118591186011861118621186311864118651186611867118681186911870118711187211873118741187511876118771187811879118801188111882118831188411885118861188711888118891189011891118921189311894118951189611897118981189911900119011190211903119041190511906119071190811909119101191111912119131191411915119161191711918119191192011921119221192311924119251192611927119281192911930119311193211933119341193511936119371193811939119401194111942119431194411945119461194711948119491195011951119521195311954119551195611957119581195911960119611196211963119641196511966119671196811969119701197111972119731197411975119761197711978119791198011981119821198311984119851198611987119881198911990119911199211993119941199511996119971199811999120001200112002120031200412005120061200712008120091201012011120121201312014120151201612017120181201912020120211202212023120241202512026120271202812029120301203112032120331203412035120361203712038120391204012041120421204312044120451204612047120481204912050120511205212053120541205512056120571205812059120601206112062120631206412065120661206712068120691207012071120721207312074120751207612077120781207912080120811208212083120841208512086120871208812089120901209112092120931209412095120961209712098120991210012101121021210312104121051210612107121081210912110121111211212113121141211512116121171211812119121201212112122121231212412125121261212712128121291213012131121321213312134121351213612137121381213912140121411214212143121441214512146121471214812149121501215112152121531215412155121561215712158121591216012161121621216312164121651216612167121681216912170121711217212173121741217512176121771217812179121801218112182121831218412185121861218712188121891219012191121921219312194121951219612197121981219912200122011220212203122041220512206122071220812209122101221112212122131221412215122161221712218122191222012221122221222312224122251222612227122281222912230122311223212233122341223512236122371223812239122401224112242122431224412245122461224712248122491225012251122521225312254122551225612257122581225912260122611226212263122641226512266122671226812269122701227112272122731227412275122761227712278122791228012281122821228312284122851228612287122881228912290122911229212293122941229512296122971229812299123001230112302123031230412305123061230712308123091231012311123121231312314123151231612317123181231912320123211232212323123241232512326123271232812329123301233112332123331233412335123361233712338123391234012341123421234312344123451234612347123481234912350123511235212353123541235512356123571235812359123601236112362123631236412365123661236712368123691237012371123721237312374123751237612377123781237912380123811238212383123841238512386123871238812389123901239112392123931239412395123961239712398123991240012401124021240312404124051240612407124081240912410124111241212413124141241512416124171241812419124201242112422124231242412425124261242712428124291243012431124321243312434124351243612437124381243912440124411244212443124441244512446124471244812449124501245112452124531245412455124561245712458124591246012461124621246312464124651246612467124681246912470124711247212473124741247512476124771247812479124801248112482124831248412485124861248712488124891249012491124921249312494124951249612497124981249912500125011250212503125041250512506125071250812509125101251112512125131251412515125161251712518125191252012521125221252312524125251252612527125281252912530125311253212533125341253512536125371253812539125401254112542125431254412545125461254712548125491255012551125521255312554125551255612557125581255912560125611256212563125641256512566125671256812569125701257112572125731257412575125761257712578125791258012581125821258312584125851258612587125881258912590125911259212593125941259512596125971259812599126001260112602126031260412605126061260712608126091261012611126121261312614126151261612617126181261912620126211262212623126241262512626126271262812629126301263112632126331263412635126361263712638126391264012641126421264312644126451264612647126481264912650126511265212653126541265512656126571265812659126601266112662126631266412665126661266712668126691267012671126721267312674126751267612677126781267912680126811268212683126841268512686126871268812689126901269112692126931269412695126961269712698126991270012701127021270312704127051270612707127081270912710127111271212713127141271512716127171271812719127201272112722127231272412725127261272712728127291273012731127321273312734127351273612737127381273912740127411274212743127441274512746127471274812749127501275112752127531275412755127561275712758127591276012761127621276312764127651276612767127681276912770127711277212773127741277512776127771277812779127801278112782127831278412785127861278712788127891279012791127921279312794127951279612797127981279912800128011280212803128041280512806128071280812809128101281112812128131281412815128161281712818128191282012821128221282312824128251282612827128281282912830128311283212833128341283512836128371283812839128401284112842128431284412845128461284712848128491285012851128521285312854128551285612857128581285912860128611286212863128641286512866128671286812869128701287112872128731287412875128761287712878128791288012881128821288312884128851288612887128881288912890128911289212893128941289512896128971289812899129001290112902129031290412905129061290712908129091291012911129121291312914129151291612917129181291912920129211292212923129241292512926129271292812929129301293112932129331293412935129361293712938129391294012941129421294312944129451294612947129481294912950129511295212953129541295512956129571295812959129601296112962129631296412965129661296712968129691297012971129721297312974129751297612977129781297912980129811298212983129841298512986129871298812989129901299112992129931299412995129961299712998129991300013001130021300313004130051300613007130081300913010130111301213013130141301513016130171301813019130201302113022130231302413025130261302713028130291303013031130321303313034130351303613037130381303913040130411304213043130441304513046130471304813049130501305113052130531305413055130561305713058130591306013061130621306313064130651306613067130681306913070130711307213073130741307513076130771307813079130801308113082130831308413085130861308713088130891309013091130921309313094130951309613097130981309913100131011310213103131041310513106131071310813109131101311113112131131311413115131161311713118131191312013121131221312313124131251312613127131281312913130131311313213133131341313513136131371313813139131401314113142131431314413145131461314713148131491315013151131521315313154131551315613157131581315913160131611316213163131641316513166131671316813169131701317113172131731317413175131761317713178131791318013181131821318313184131851318613187131881318913190131911319213193131941319513196131971319813199132001320113202132031320413205132061320713208132091321013211132121321313214132151321613217132181321913220132211322213223132241322513226132271322813229132301323113232132331323413235132361323713238132391324013241132421324313244132451324613247132481324913250132511325213253132541325513256132571325813259132601326113262132631326413265132661326713268132691327013271132721327313274132751327613277132781327913280132811328213283132841328513286132871328813289132901329113292132931329413295132961329713298132991330013301133021330313304133051330613307133081330913310133111331213313133141331513316133171331813319133201332113322133231332413325133261332713328133291333013331133321333313334133351333613337133381333913340133411334213343133441334513346133471334813349133501335113352133531335413355133561335713358133591336013361133621336313364133651336613367133681336913370133711337213373133741337513376133771337813379133801338113382133831338413385133861338713388133891339013391133921339313394133951339613397133981339913400134011340213403134041340513406134071340813409134101341113412134131341413415134161341713418134191342013421134221342313424134251342613427134281342913430134311343213433134341343513436134371343813439134401344113442134431344413445134461344713448134491345013451134521345313454134551345613457134581345913460134611346213463134641346513466134671346813469134701347113472134731347413475134761347713478134791348013481134821348313484134851348613487134881348913490134911349213493134941349513496134971349813499135001350113502135031350413505135061350713508135091351013511135121351313514135151351613517135181351913520135211352213523135241352513526135271352813529135301353113532135331353413535135361353713538135391354013541135421354313544135451354613547135481354913550135511355213553135541355513556135571355813559135601356113562135631356413565135661356713568135691357013571135721357313574135751357613577135781357913580135811358213583135841358513586135871358813589135901359113592135931359413595135961359713598135991360013601136021360313604136051360613607136081360913610136111361213613136141361513616136171361813619136201362113622136231362413625136261362713628136291363013631136321363313634136351363613637136381363913640136411364213643136441364513646136471364813649136501365113652136531365413655136561365713658136591366013661136621366313664136651366613667136681366913670136711367213673136741367513676136771367813679136801368113682136831368413685136861368713688136891369013691136921369313694136951369613697136981369913700137011370213703137041370513706137071370813709137101371113712137131371413715137161371713718137191372013721137221372313724137251372613727137281372913730137311373213733137341373513736137371373813739137401374113742137431374413745137461374713748137491375013751137521375313754137551375613757137581375913760137611376213763137641376513766137671376813769137701377113772137731377413775137761377713778137791378013781137821378313784137851378613787137881378913790137911379213793137941379513796137971379813799138001380113802138031380413805138061380713808138091381013811138121381313814138151381613817138181381913820138211382213823138241382513826138271382813829138301383113832138331383413835138361383713838138391384013841138421384313844138451384613847138481384913850138511385213853138541385513856138571385813859138601386113862138631386413865138661386713868138691387013871138721387313874138751387613877138781387913880138811388213883138841388513886138871388813889138901389113892138931389413895138961389713898138991390013901139021390313904139051390613907139081390913910139111391213913139141391513916139171391813919139201392113922139231392413925139261392713928139291393013931139321393313934139351393613937139381393913940139411394213943139441394513946139471394813949139501395113952139531395413955139561395713958139591396013961139621396313964139651396613967139681396913970139711397213973139741397513976139771397813979139801398113982139831398413985139861398713988139891399013991139921399313994139951399613997139981399914000140011400214003140041400514006140071400814009140101401114012140131401414015140161401714018140191402014021140221402314024140251402614027140281402914030140311403214033140341403514036140371403814039140401404114042140431404414045140461404714048140491405014051140521405314054140551405614057140581405914060140611406214063140641406514066140671406814069140701407114072140731407414075140761407714078140791408014081140821408314084140851408614087140881408914090140911409214093140941409514096140971409814099141001410114102141031410414105141061410714108141091411014111141121411314114141151411614117141181411914120141211412214123141241412514126141271412814129141301413114132141331413414135141361413714138141391414014141141421414314144141451414614147141481414914150141511415214153141541415514156141571415814159141601416114162141631416414165141661416714168141691417014171141721417314174141751417614177141781417914180141811418214183141841418514186141871418814189141901419114192141931419414195141961419714198141991420014201142021420314204142051420614207142081420914210142111421214213142141421514216142171421814219142201422114222142231422414225142261422714228142291423014231142321423314234142351423614237142381423914240142411424214243142441424514246142471424814249142501425114252142531425414255142561425714258142591426014261142621426314264142651426614267142681426914270142711427214273142741427514276142771427814279142801428114282142831428414285142861428714288142891429014291142921429314294142951429614297142981429914300143011430214303143041430514306143071430814309143101431114312143131431414315143161431714318143191432014321143221432314324143251432614327143281432914330143311433214333143341433514336143371433814339143401434114342143431434414345143461434714348143491435014351143521435314354143551435614357143581435914360143611436214363143641436514366143671436814369143701437114372143731437414375143761437714378143791438014381143821438314384143851438614387143881438914390143911439214393143941439514396143971439814399144001440114402144031440414405144061440714408144091441014411144121441314414144151441614417144181441914420144211442214423144241442514426144271442814429144301443114432144331443414435144361443714438144391444014441144421444314444144451444614447144481444914450144511445214453144541445514456144571445814459144601446114462144631446414465144661446714468144691447014471144721447314474144751447614477144781447914480144811448214483144841448514486144871448814489144901449114492144931449414495144961449714498144991450014501145021450314504145051450614507145081450914510145111451214513145141451514516145171451814519145201452114522145231452414525145261452714528145291453014531145321453314534145351453614537145381453914540145411454214543145441454514546145471454814549145501455114552145531455414555145561455714558145591456014561145621456314564145651456614567145681456914570145711457214573145741457514576145771457814579145801458114582145831458414585145861458714588145891459014591145921459314594145951459614597145981459914600146011460214603146041460514606146071460814609146101461114612146131461414615146161461714618146191462014621146221462314624146251462614627146281462914630146311463214633146341463514636146371463814639146401464114642146431464414645146461464714648146491465014651146521465314654146551465614657146581465914660146611466214663146641466514666146671466814669146701467114672146731467414675146761467714678146791468014681146821468314684146851468614687146881468914690146911469214693146941469514696146971469814699147001470114702147031470414705147061470714708147091471014711147121471314714147151471614717147181471914720147211472214723147241472514726147271472814729147301473114732147331473414735147361473714738147391474014741147421474314744147451474614747147481474914750147511475214753147541475514756147571475814759147601476114762147631476414765147661476714768147691477014771147721477314774147751477614777147781477914780147811478214783147841478514786147871478814789147901479114792147931479414795147961479714798147991480014801148021480314804148051480614807148081480914810148111481214813148141481514816148171481814819148201482114822148231482414825148261482714828148291483014831148321483314834148351483614837148381483914840148411484214843148441484514846148471484814849148501485114852148531485414855148561485714858148591486014861148621486314864148651486614867148681486914870148711487214873148741487514876148771487814879148801488114882148831488414885148861488714888148891489014891148921489314894148951489614897148981489914900149011490214903149041490514906149071490814909149101491114912149131491414915149161491714918149191492014921149221492314924149251492614927149281492914930149311493214933149341493514936149371493814939149401494114942149431494414945149461494714948149491495014951149521495314954149551495614957149581495914960149611496214963149641496514966149671496814969149701497114972149731497414975149761497714978149791498014981149821498314984149851498614987149881498914990149911499214993149941499514996149971499814999150001500115002150031500415005150061500715008150091501015011150121501315014
  1. #!/usr/bin/perl -T
  2. #------------------------------------------------------------------------------
  3. # This is amavisd-new.
  4. # It is an interface between message transfer agent (MTA) and virus
  5. # scanners and/or spam scanners, functioning as a mail content filter.
  6. #
  7. # It is a performance-enhanced and feature-enriched version of amavisd
  8. # (which in turn is a daemonized version of AMaViS), initially based
  9. # on amavisd-snapshot-20020300).
  10. #
  11. # All work since amavisd-snapshot-20020300:
  12. # Copyright (C) 2002,2003,2004,2005 Mark Martinec, All Rights Reserved.
  13. # with contributions from the amavis-* mailing lists and individuals,
  14. # as acknowledged in the release notes.
  15. #
  16. # This program is free software; you can redistribute it and/or modify
  17. # it under the terms of the GNU General Public License as published by
  18. # the Free Software Foundation; either version 2 of the License, or
  19. # (at your option) any later version.
  20. #
  21. # This program is distributed in the hope that it will be useful,
  22. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  23. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  24. # GNU General Public License for details.
  25. #
  26. # You should have received a copy of the GNU General Public License
  27. # along with this program; if not, write to the Free Software
  28. # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
  29. # Author: Mark Martinec <mark.martinec@ijs.si>
  30. # Patches and problem reports are welcome.
  31. #
  32. # The latest version of this program is available at:
  33. # http://www.ijs.si/software/amavisd/
  34. #------------------------------------------------------------------------------
  35. # Here is a boilerplate from the amavisd(-snapshot) version,
  36. # which is the version that served as a base code for the initial
  37. # version of amavisd-new. License terms were the same:
  38. #
  39. # Author: Chris Mason <cmason@unixzone.com>
  40. # Current maintainer: Lars Hecking <lhecking@users.sourceforge.net>
  41. # Based on work by:
  42. # Mogens Kjaer, Carlsberg Laboratory, <mk@crc.dk>
  43. # Juergen Quade, Softing GmbH, <quade@softing.com>
  44. # Christian Bricart <shiva@aachalon.de>
  45. # Rainer Link <link@foo.fh-furtwangen.de>
  46. # This script is part of the AMaViS package. For more information see:
  47. # http://amavis.org/
  48. # Copyright (C) 2000 - 2002 the people mentioned above
  49. # This software is licensed under the GNU General Public License (GPL)
  50. # See: http://www.gnu.org/copyleft/gpl.html
  51. #------------------------------------------------------------------------------
  52. #------------------------------------------------------------------------------
  53. #Index of packages in this file
  54. # Amavis::Boot
  55. # Amavis::Conf
  56. # Amavis::Lock
  57. # Amavis::Log
  58. # Amavis::Timing
  59. # Amavis::Util
  60. # Amavis::rfc2821_2822_Tools
  61. # Amavis::Lookup::RE
  62. # Amavis::Lookup::IP
  63. # Amavis::Lookup::Label
  64. # Amavis::Lookup
  65. # Amavis::Expand
  66. # Amavis::IO::Zlib
  67. # Amavis::In::Connection
  68. # Amavis::In::Message::PerRecip
  69. # Amavis::In::Message
  70. # Amavis::Out::EditHeader
  71. # Amavis::Out::Local
  72. # Amavis::Out
  73. # Amavis::UnmangleSender
  74. # Amavis::Unpackers::NewFilename
  75. # Amavis::Unpackers::Part
  76. # Amavis::Unpackers::OurFiler
  77. # Amavis::Unpackers::Validity
  78. # Amavis::Unpackers::MIME
  79. # Amavis::Notify
  80. # Amavis::Cache
  81. # Amavis
  82. #optionally compiled-in packages: ---------------------------------------------
  83. # Amavis::DB::SNMP
  84. # Amavis::DB
  85. # Amavis::Cache
  86. # Amavis::Out::SQL::Connection
  87. # Amavis::Out::SQL::Log
  88. # Amavis::IO::SQL
  89. # Amavis::Out::SQL::Quarantine
  90. # Amavis::Lookup::SQLfield
  91. # Amavis::Lookup::SQL
  92. # Amavis::LDAP::Connection
  93. # Amavis::Lookup::LDAP
  94. # Amavis::Lookup::LDAPattr
  95. # Amavis::In::AMCL
  96. # Amavis::In::SMTP
  97. # Amavis::AV
  98. # Amavis::SpamControl
  99. # Amavis::Unpackers
  100. #------------------------------------------------------------------------------
  101. #
  102. package Amavis::Boot;
  103. use strict;
  104. use re 'taint';
  105. # Fetch all required modules (or nicely report missing ones), and compile them
  106. # once-and-for-all at the parent process, so that forked children can inherit
  107. # and share already compiled code in memory. Children will still need to 'use'
  108. # modules if they want to inherit from their name space.
  109. #
  110. sub fetch_modules($$@) {
  111. my($reason, $required, @modules) = @_;
  112. my(@missing);
  113. for my $m (@modules) {
  114. local($_) = $m;
  115. $_ .= /^auto::/ ? '.al' : '.pm' if !/\.(pm|pl|al)\z/;
  116. s[::][/]g;
  117. eval { require $_ } or push(@missing, $m);
  118. }
  119. die "ERROR: MISSING $reason:\n" . join('', map { " $_\n" } @missing)
  120. if $required && @missing;
  121. \@missing;
  122. }
  123. BEGIN {
  124. fetch_modules('REQUIRED BASIC MODULES', 1, qw(
  125. Exporter POSIX Fcntl Socket Errno Carp Time::HiRes
  126. IO::Handle IO::File IO::Socket IO::Socket::UNIX IO::Socket::INET
  127. IO::Wrap IO::Stringy Digest::MD5 Unix::Syslog File::Basename
  128. Mail::Field Mail::Address Mail::Header Mail::Internet Compress::Zlib
  129. MIME::Base64 MIME::QuotedPrint MIME::Words
  130. MIME::Head MIME::Body MIME::Entity MIME::Parser MIME::Decoder
  131. MIME::Decoder::Base64 MIME::Decoder::Binary MIME::Decoder::QuotedPrint
  132. MIME::Decoder::NBit MIME::Decoder::UU MIME::Decoder::Gzip64
  133. Net::Cmd Net::SMTP Net::Server Net::Server::PreForkSimple
  134. ));
  135. # with earlier versions of Perl one may need to add additional modules
  136. # to the list, such as: auto::POSIX::setgid auto::POSIX::setuid ...
  137. fetch_modules('OPTIONAL BASIC MODULES', 0, qw(
  138. Carp::Heavy auto::POSIX::setgid auto::POSIX::setuid
  139. MIME::Decoder::BinHex
  140. ));
  141. }
  142. 1;
  143. #
  144. package Amavis::Conf;
  145. use strict;
  146. use re 'taint';
  147. # prototypes
  148. sub D_REJECT();
  149. sub D_BOUNCE();
  150. sub D_DISCARD();
  151. sub D_PASS();
  152. BEGIN {
  153. use Exporter ();
  154. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  155. $VERSION = '2.043';
  156. @ISA = qw(Exporter);
  157. %EXPORT_TAGS = (
  158. 'dynamic_confvars' => [qw(
  159. $policy_bank_name $protocol @inet_acl
  160. $log_level $log_templ $log_recip_templ $forward_method $notify_method
  161. $amavis_auth_user $amavis_auth_pass $auth_reauthenticate_forwarded
  162. $auth_required_out $auth_required_inp $auth_required_release
  163. @auth_mech_avail
  164. $local_client_bind_address
  165. $localhost_name $smtpd_greeting_banner $smtpd_quit_banner
  166. $smtpd_message_size_limit
  167. $final_virus_destiny $final_spam_destiny
  168. $final_banned_destiny $final_bad_header_destiny
  169. $warnvirussender $warnspamsender $warnbannedsender $warnbadhsender
  170. $warn_offsite
  171. @av_scanners @av_scanners_backup $first_infected_stops_scan
  172. $bypass_decode_parts @decoders
  173. $defang_virus $defang_banned $defang_spam
  174. $defang_bad_header $defang_undecipherable $defang_all
  175. $undecipherable_subject_tag
  176. $sa_spam_report_header $sa_spam_level_char
  177. $sa_mail_body_size_limit
  178. $localpart_is_case_sensitive
  179. $recipient_delimiter $replace_existing_extension
  180. $hdr_encoding $bdy_encoding $hdr_encoding_qb
  181. $notify_xmailer_header $X_HEADER_TAG $X_HEADER_LINE
  182. $remove_existing_x_scanned_headers $remove_existing_spam_headers
  183. $hdrfrom_notify_sender $hdrfrom_notify_recip
  184. $hdrfrom_notify_admin $hdrfrom_notify_spamadmin
  185. $mailfrom_notify_sender $mailfrom_notify_recip
  186. $mailfrom_notify_admin $mailfrom_notify_spamadmin
  187. $mailfrom_to_quarantine
  188. $virus_quarantine_method $spam_quarantine_method
  189. $banned_files_quarantine_method $bad_header_quarantine_method
  190. %local_delivery_aliases
  191. $notify_sender_templ
  192. $notify_virus_sender_templ $notify_spam_sender_templ
  193. $notify_virus_admin_templ $notify_spam_admin_templ
  194. $notify_virus_recips_templ $notify_spam_recips_templ
  195. $banned_namepath_re
  196. $per_recip_whitelist_sender_lookup_tables
  197. $per_recip_blacklist_sender_lookup_tables
  198. %sql_clause
  199. @local_domains_maps @mynetworks_maps
  200. @bypass_virus_checks_maps @bypass_spam_checks_maps
  201. @bypass_banned_checks_maps @bypass_header_checks_maps
  202. @virus_lovers_maps @spam_lovers_maps
  203. @banned_files_lovers_maps @bad_header_lovers_maps
  204. @warnvirusrecip_maps @warnbannedrecip_maps @warnbadhrecip_maps
  205. @newvirus_admin_maps @virus_admin_maps
  206. @banned_admin_maps @bad_header_admin_maps @spam_admin_maps
  207. @virus_quarantine_to_maps
  208. @banned_quarantine_to_maps @bad_header_quarantine_to_maps
  209. @spam_quarantine_to_maps @spam_quarantine_bysender_to_maps
  210. @banned_filename_maps
  211. @spam_tag_level_maps @spam_tag2_level_maps @spam_kill_level_maps
  212. @spam_dsn_cutoff_level_maps @spam_quarantine_cutoff_level_maps
  213. @spam_modifies_subj_maps @spam_subject_tag_maps @spam_subject_tag2_maps
  214. @whitelist_sender_maps @blacklist_sender_maps @score_sender_maps
  215. @message_size_limit_maps
  216. @addr_extension_virus_maps @addr_extension_spam_maps
  217. @addr_extension_banned_maps @addr_extension_bad_header_maps
  218. @debug_sender_maps %recipient_policy_bank_map %recipient_policy_bank_re_map $sa_site_rules_filename
  219. )],
  220. 'confvars' => [qw(
  221. $myproduct_name $myversion_id $myversion_id_numeric $myversion_date
  222. $myversion $myhostname
  223. $MYHOME $TEMPBASE $QUARANTINEDIR $quarantine_subdir_levels
  224. $daemonize $pid_file $lock_file $db_home
  225. $enable_db $enable_global_cache
  226. $daemon_user $daemon_group $daemon_chroot_dir $path
  227. $DEBUG $DO_SYSLOG $SYSLOG_LEVEL $LOGFILE
  228. $max_servers $max_requests $child_timeout
  229. %current_policy_bank %policy_bank %interface_policy
  230. $unix_socketname $inet_socket_port $inet_socket_bind
  231. $insert_received_line $relayhost_is_client $smtpd_recipient_limit
  232. $MAXLEVELS $MAXFILES
  233. $MIN_EXPANSION_QUOTA $MIN_EXPANSION_FACTOR
  234. $MAX_EXPANSION_QUOTA $MAX_EXPANSION_FACTOR
  235. @lookup_sql_dsn @storage_sql_dsn
  236. $virus_check_negative_ttl $virus_check_positive_ttl
  237. $spam_check_negative_ttl $spam_check_positive_ttl
  238. $enable_ldap $default_ldap
  239. @keep_decoded_original_maps @map_full_type_to_short_type_maps
  240. @viruses_that_fake_sender_maps %banned_rules
  241. $file %recipient_policy_bank_map %recipient_policy_bank_re_map $sa_site_rules_filename
  242. )],
  243. 'sa' => [qw(
  244. $helpers_home $dspam
  245. $sa_local_tests_only $sa_auto_whitelist $sa_timeout $sa_debug
  246. $sa_site_rules_filename
  247. )],
  248. 'platform' => [qw(
  249. $can_truncate $unicode_aware $eol
  250. &D_REJECT &D_BOUNCE &D_DISCARD &D_PASS
  251. )],
  252. # other variables settable by user in amavisd.conf,
  253. # but not directly accessible by the program
  254. 'hidden_confvars' => [qw(
  255. $mydomain
  256. )],
  257. # legacy variables, predeclared for compatibility of amavisd.conf
  258. # The rest of the program does not use them directly and they should not be
  259. # visible in other modules, but may be referenced through @*_maps variables
  260. 'legacy_confvars' => [qw(
  261. %local_domains @local_domains_acl $local_domains_re @mynetworks
  262. %bypass_virus_checks @bypass_virus_checks_acl $bypass_virus_checks_re
  263. %bypass_spam_checks @bypass_spam_checks_acl $bypass_spam_checks_re
  264. %bypass_banned_checks @bypass_banned_checks_acl $bypass_banned_checks_re
  265. %bypass_header_checks @bypass_header_checks_acl $bypass_header_checks_re
  266. %virus_lovers @virus_lovers_acl $virus_lovers_re
  267. %spam_lovers @spam_lovers_acl $spam_lovers_re
  268. %banned_files_lovers @banned_files_lovers_acl $banned_files_lovers_re
  269. %bad_header_lovers @bad_header_lovers_acl $bad_header_lovers_re
  270. %virus_admin %spam_admin
  271. $newvirus_admin $virus_admin $banned_admin $bad_header_admin $spam_admin
  272. $warnvirusrecip $warnbannedrecip $warnbadhrecip
  273. $virus_quarantine_to $banned_quarantine_to $bad_header_quarantine_to
  274. $spam_quarantine_to $spam_quarantine_bysender_to
  275. $keep_decoded_original_re $map_full_type_to_short_type_re
  276. $banned_filename_re $viruses_that_fake_sender_re
  277. $sa_tag_level_deflt $sa_tag2_level_deflt $sa_kill_level_deflt
  278. $sa_dsn_cutoff_level $sa_quarantine_cutoff_level
  279. $sa_spam_modifies_subj $sa_spam_subject_tag1 $sa_spam_subject_tag
  280. %whitelist_sender @whitelist_sender_acl $whitelist_sender_re
  281. %blacklist_sender @blacklist_sender_acl $blacklist_sender_re
  282. $addr_extension_virus $addr_extension_spam
  283. $addr_extension_banned $addr_extension_bad_header
  284. $sql_select_policy $sql_select_white_black_list
  285. $gets_addr_in_quoted_form @debug_sender_acl
  286. $arc $bzip2 $lzop $lha $unarj $gzip $uncompress $unfreeze
  287. $unrar $zoo $pax $cpio $ar $rpm2cpio $cabextract $ripole $tnef
  288. $gunzip $bunzip2 $unlzop
  289. )],
  290. );
  291. Exporter::export_tags qw(dynamic_confvars confvars sa platform
  292. hidden_confvars legacy_confvars);
  293. } # BEGIN
  294. use POSIX ();
  295. use Carp ();
  296. use Errno qw(ENOENT EACCES);
  297. use vars @EXPORT;
  298. sub c($); sub cr($); sub ca($); # prototypes
  299. use subs qw(c cr ca); # access subroutine to new-style config variables
  300. BEGIN { push(@EXPORT,qw(c cr ca)) }
  301. { # initialize policy bank hash containing dynamic config settings
  302. for my $tag (@EXPORT_TAGS{'dynamic_confvars'}) {
  303. for my $v (@$tag) {
  304. if ($v !~ /^([%\$\@])(.*)\z/) { die "Unsupported variable type: $v" }
  305. else {
  306. no strict 'refs'; my($type,$name) = ($1,$2);
  307. $current_policy_bank{$name} = $type eq '$' ? \${"Amavis::Conf::$name"}
  308. : $type eq '@' ? \@{"Amavis::Conf::$name"}
  309. : $type eq '%' ? \%{"Amavis::Conf::$name"}
  310. : undef;
  311. }
  312. }
  313. }
  314. $current_policy_bank{'policy_bank_name'} = ''; # builtin policy
  315. $current_policy_bank{'policy_bank_path'} = '';
  316. $policy_bank{''} = { %current_policy_bank }; # copy
  317. }
  318. # new-style access to dynamic config variables
  319. # return a config variable value - usually a scalar;
  320. # one level of indirection for scalars is allowed
  321. sub c($) {
  322. my($name) = @_;
  323. if (!exists $current_policy_bank{$name}) {
  324. Carp::croak(sprintf('No entry "%s" in policy bank "%s"',
  325. $name, $current_policy_bank{'policy_bank_name'}));
  326. }
  327. my($var) = $current_policy_bank{$name}; my($r) = ref($var);
  328. !$r ? $var : $r eq 'SCALAR' ? $$var
  329. : $r eq 'ARRAY' ? @$var : $r eq 'HASH' ? %$var : $var;
  330. }
  331. # return a ref to a config variable value, or undef if var is undefined
  332. sub cr($) {
  333. my($name) = @_;
  334. if (!exists $current_policy_bank{$name}) {
  335. Carp::croak(sprintf('No entry "%s" in policy bank "%s"',
  336. $name, $current_policy_bank{'policy_bank_name'}));
  337. }
  338. my($var) = $current_policy_bank{$name};
  339. !defined($var) ? undef : !ref($var) ? \$var : $var;
  340. }
  341. # return a ref to a config variable value (which is supposed to be an array),
  342. # converting undef to an empty array, and a scalar to a one-element array
  343. # if necessary
  344. sub ca($) {
  345. my($name) = @_;
  346. if (!exists $current_policy_bank{$name}) {
  347. Carp::croak(sprintf('No entry "%s" in policy bank "%s"',
  348. $name, $current_policy_bank{'policy_bank_name'}));
  349. }
  350. my($var) = $current_policy_bank{$name};
  351. !defined($var) ? [] : !ref($var) ? [$var] : $var;
  352. }
  353. $myproduct_name = 'amavisd-new';
  354. $myversion_id = '2.3.3'; $myversion_date = '20050822';
  355. $myversion = "$myproduct_name-$myversion_id ($myversion_date)";
  356. $myversion_id_numeric = # x.yyyzzz, allows numerical comparision, like Perl $]
  357. sprintf("%8.6f", $1 + ($2 + $3/1000)/1000)
  358. if $myversion_id =~ /^(\d+)(?:\.(\d*)(?:\.(\d*))?)?(.*)$/;
  359. $eol = "\n"; # native record separator in files: LF or CRLF or even CR
  360. $unicode_aware = $]>=5.008 && length("\x{263a}")==1 && eval { require Encode };
  361. # serves only as a quick default for other configuration settings
  362. $MYHOME = '/var/amavis';
  363. $mydomain = '!change-mydomain-variable!.example.com';#intentionally bad default
  364. # Create debugging output - true: log to stderr; false: log to syslog/file
  365. $DEBUG = 0;
  366. # Cause Net::Server parameters 'background' and 'setsid' to be set,
  367. # resulting in the program to detach itself from the terminal
  368. $daemonize = 1;
  369. # Net::Server pre-forking settings - defaults, overruled by amavisd.conf
  370. $max_servers = 2; # number of pre-forked children
  371. $max_requests = 10; # retire a child after that many accepts
  372. $child_timeout = 8*60; # abort child if it does not complete each task in n sec
  373. # Can file be truncated?
  374. # Set to 1 if 'truncate' works (it is XPG4-UNIX standard feature,
  375. # not required by Posix).
  376. # Things will go faster with SMTP-in, otherwise (e.g. with milter)
  377. # it makes no difference as file truncation will not be used.
  378. $can_truncate = 1;
  379. # expiration time of cached results: time to live in seconds
  380. # (how long the result of a virus/spam test remains valid)
  381. $virus_check_negative_ttl= 3*60; # time to remember that mail was not infected
  382. $virus_check_positive_ttl= 30*60; # time to remember that mail was infected
  383. $spam_check_negative_ttl = 30*60; # time to remember that mail was not spam
  384. $spam_check_positive_ttl = 30*60; # time to remember that mail was spam
  385. #
  386. # NOTE:
  387. # Cache size will be determined by the largest of the $*_ttl values.
  388. # Depending on the mail rate, the cache database may grow quite large.
  389. # Reasonable compromise for the max value is 15 minutes to 2 hours.
  390. # Customizable notification messages, logging
  391. $SYSLOG_LEVEL = 'mail.debug';
  392. $enable_db = 0; # load optional modules Amavis::DB & Amavis::DB::SNMP
  393. $enable_global_cache = 0; # enable use of bdb-based Amavis::Cache
  394. # Where to find SQL server(s) and database to support SQL lookups?
  395. # A list of triples: (dsn,user,passw). Specify more than one
  396. # for multiple (backup) SQL servers.
  397. #
  398. #@storage_sql_dsn =
  399. #@lookup_sql_dsn =
  400. # ( ['DBI:mysql:mail:host1', 'some-username1', 'some-password1'],
  401. # ['DBI:mysql:mail:host2', 'some-username2', 'some-password2'] );
  402. # The SQL select clause to fetch per-recipient policy settings
  403. # The %k will be replaced by a comma-separated list of query addresses
  404. # (e.g. full address, domain only, catchall). Use ORDER, if there
  405. # is a chance that multiple records will match - the first match wins
  406. # If field names are not unique (e.g. 'id'), the later field overwrites the
  407. # earlier in a hash returned by lookup, which is why we use '*,users.id'.
  408. $sql_select_policy =
  409. 'SELECT *,users.id FROM users LEFT JOIN policy ON users.policy_id=policy.id'.
  410. ' WHERE users.email IN (%k) ORDER BY users.priority DESC';
  411. # The SQL select clause to check sender in per-recipient whitelist/blacklist
  412. # The first SELECT argument '?' will be users.id from recipient SQL lookup,
  413. # the %k will be sender addresses (e.g. full address, domain only, catchall).
  414. # Only the first occurrence of '?' will be replaced by users.id, subsequent
  415. # occurrences of '?' will see empty string as an argument. There can be zero
  416. # or more occurrences of %k, lookup keys will be multiplied accordingly.
  417. # Up until version 2.2.0 the '?' had to be placed before the '%k';
  418. # starting with 2.2.1 this restriction is lifted.
  419. $sql_select_white_black_list =
  420. 'SELECT wb FROM wblist LEFT JOIN mailaddr ON wblist.sid=mailaddr.id'.
  421. ' WHERE (wblist.rid=?) AND (mailaddr.email IN (%k))'.
  422. ' ORDER BY mailaddr.priority DESC';
  423. %sql_clause = (
  424. 'sel_policy' => \$sql_select_policy,
  425. 'sel_wblist' => \$sql_select_white_black_list,
  426. 'sel_adr' =>
  427. 'SELECT id FROM maddr WHERE email=?',
  428. 'ins_adr' =>
  429. 'INSERT INTO maddr (email, domain) VALUES (?,?)',
  430. 'ins_msg' =>
  431. 'INSERT INTO msgs (mail_id, secret_id, am_id, time_num, time_iso, sid,'.
  432. ' policy, client_addr, size, host) VALUES (?,?,?,?,?,?,?,?,?,?)',
  433. 'upd_msg' =>
  434. 'UPDATE msgs SET content=?, quar_type=?, dsn_sent=?, spam_level=?,'.
  435. ' message_id=?, from_addr=?, subject=? WHERE mail_id=?',
  436. 'ins_rcp' =>
  437. 'INSERT INTO msgrcpt (mail_id, rid, ds, rs, bl, wl, bspam_level,'.
  438. ' smtp_resp) VALUES (?,?,?,?,?,?,?,?)',
  439. 'ins_quar' =>
  440. 'INSERT INTO quarantine (mail_id, chunk_ind, mail_text) VALUES (?,?,?)',
  441. 'sel_quar' =>
  442. 'SELECT mail_text FROM quarantine WHERE mail_id=? ORDER BY chunk_ind',
  443. );
  444. #
  445. # Receiving mail related
  446. # $unix_socketname = '/var/amavis/amavisd.sock'; # traditional amavis client protocol
  447. # $inet_socket_port = 10024; # accept SMTP on this TCP port
  448. # $inet_socket_port = [10024,10026,10027]; # ...possibly on more than one
  449. $inet_socket_bind = '127.0.0.1'; # limit socket bind to loopback interface
  450. @inet_acl = qw( 127.0.0.1 [::1] ); # allow SMTP access only from localhost
  451. @mynetworks = qw( 127.0.0.0/8 [::1] [FE80::]/10 [FEC0::]/10
  452. 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 );
  453. $notify_method = 'smtp:[127.0.0.1]:10025';
  454. $forward_method = 'smtp:[127.0.0.1]:10025';
  455. #old defaults:
  456. # $virus_quarantine_method = 'local:virus-%i-%n';
  457. # $spam_quarantine_method = 'local:spam-%b-%i-%n.gz';
  458. # $banned_files_quarantine_method = 'local:banned-%i-%n';
  459. # $bad_header_quarantine_method = 'local:badh-%i-%n';
  460. #new defaults:
  461. $virus_quarantine_method = 'local:virus-%m';
  462. $spam_quarantine_method = 'local:spam-%m.gz';
  463. $banned_files_quarantine_method = 'local:banned-%m';
  464. $bad_header_quarantine_method = 'local:badh-%m';
  465. $insert_received_line = 1; # insert 'Received:' header field? (not with milter)
  466. $remove_existing_x_scanned_headers = 0;
  467. $remove_existing_spam_headers = 1;
  468. # encoding (charset in MIME terminology)
  469. # to be used in RFC 2047-encoded ...
  470. $hdr_encoding = 'iso-8859-1'; # ... header field bodies
  471. $bdy_encoding = 'iso-8859-1'; # ... notification body text
  472. # encoding (encoding in MIME terminology)
  473. $hdr_encoding_qb = 'Q'; # quoted-printable (default)
  474. #$hdr_encoding_qb = 'B'; # base64 (usual for far east charsets)
  475. $smtpd_recipient_limit = 1100; # max recipients (RCPT TO) - sanity limit
  476. # $myhostname is used by SMTP server module in the initial SMTP welcome line,
  477. # in inserted 'Received:' lines, Message-ID in notifications, log entries, ...
  478. $myhostname = (POSIX::uname)[1]; # should be a FQDN !
  479. $smtpd_greeting_banner = '${helo-name} ${protocol} ${product} service ready';
  480. $smtpd_quit_banner = '${helo-name} ${product} closing transmission channel';
  481. # $localhost_name is the name of THIS host running amavisd
  482. # (typically 'localhost'). It is used in HELO SMTP command
  483. # when reinjecting mail back to MTA via SMTP for final delivery.
  484. $localhost_name = 'localhost';
  485. # @auth_mech_avail = ('PLAIN','LOGIN'); # empty list disables incoming AUTH
  486. #$auth_required_inp = 1; # incoming SMTP authentication required by amavisd?
  487. #$auth_required_out = 1; # SMTP authentication required by MTA
  488. $auth_required_release = 1; # secret_id is required for a quarantine release
  489. # SMTP AUTH username and password for notification submissions
  490. # (and reauthentication of forwarded mail if requested)
  491. #$amavis_auth_user = undef; # perhaps: 'amavisd'
  492. #$amavis_auth_pass = undef;
  493. #$auth_reauthenticate_forwarded = undef; # supply our own credentials also
  494. # for forwarded (passed) mail
  495. # whom quarantined messages appear to be sent from (envelope sender)
  496. # $mailfrom_to_quarantine = undef; # original sender if undef, or set explicitly
  497. # where to send quarantined malware
  498. # Specify undef to disable, or e-mail address containing '@',
  499. # or just a local part, which will be mapped by %local_delivery_aliases
  500. # into local mailbox name or directory. The lookup key is a recipient address
  501. $virus_quarantine_to = 'virus-quarantine'; # %local_delivery_aliases mapped
  502. $banned_quarantine_to = 'banned-quarantine'; # %local_delivery_aliases mapped
  503. $bad_header_quarantine_to = 'bad-header-quarantine'; # %local_delivery_aliases
  504. $spam_quarantine_to = 'spam-quarantine'; # %local_delivery_aliases mapped
  505. $banned_admin = \@virus_admin_maps; # compatibility
  506. $bad_header_admin = \@virus_admin_maps; # compatibility
  507. # similar to $spam_quarantine_to, but the lookup key is the sender address
  508. $spam_quarantine_bysender_to = undef; # dflt: no by-sender spam quarantine
  509. # quarantine directory or mailbox file or empty
  510. # (only used if $virus_quarantine_to specifies direct local delivery)
  511. $QUARANTINEDIR = undef; # no quarantine unless overridden by config
  512. $undecipherable_subject_tag = '***UNCHECKED*** ';
  513. # string to prepend to Subject header field when message qualifies as spam
  514. # $sa_spam_subject_tag1 = undef; # example: '***possible SPAM*** '
  515. # $sa_spam_subject_tag = undef; # example: '***SPAM*** '
  516. $sa_spam_modifies_subj = 1; # true for compatibility; can be a
  517. # lookup table indicating per-recip settings
  518. $sa_spam_level_char = '*'; # character to be used in X-Spam-Level bar;
  519. # empty or undef disables adding this header field
  520. # $sa_spam_report_header = undef; # insert X-Spam-Report header field?
  521. $sa_local_tests_only = 0;
  522. $sa_debug = undef;
  523. $sa_timeout = 30; # timeout in seconds for a call to SpamAssassin
  524. # MIME defanging is only done when enabled and malware is allowed to pass
  525. # $defang_virus = undef;
  526. # $defang_banned = undef;
  527. # $defang_spam = undef;
  528. # $defang_bad_header = undef;
  529. # $defang_undecipherable = undef;
  530. # $defang_all = undef;
  531. $file = 'file'; # path to the file(1) utility for classifying contents
  532. $MIN_EXPANSION_FACTOR = 5; # times original mail size
  533. $MAX_EXPANSION_FACTOR = 500; # times original mail size
  534. # See amavisd.conf and README.lookups for details.
  535. # What to do with the message (this is independent of quarantining):
  536. # Reject: tell MTA to generate a non-delivery notification, MTA gets 5xx
  537. # Bounce: generate a non-delivery notification by ourselves, MTA gets 250
  538. # Discard: drop the message and pretend it was delivered, MTA gets 250
  539. # Pass: deliver/accept the message
  540. #
  541. # Bounce and Reject are similar: in both cases sender gets a non-delivery
  542. # notification, either generated by amavisd-new, or by MTA. The notification
  543. # issued by amavisd-new may be more informative, while on the other hand
  544. # MTA may be able to do a true reject on the original SMTP session
  545. # (e.g. with sendmail milter), or else it just generates normal non-delivery
  546. # notification / bounce (e.g. with Postfix, Exim). As a consequence,
  547. # with Postfix and Exim and dual-sendmail setup the Bounce is more informative
  548. # than Reject, but sendmail-milter users may prefer Reject.
  549. #
  550. # Bounce and Discard are similar: in both cases amavisd-new confirms
  551. # to MTA the message reception with success code 250. The difference is
  552. # in sender notification: Bounce sends a non-delivery notification to sender,
  553. # Discard does not, the message is silently dropped. Quarantine and
  554. # admin notifications are not affected by any of these settings.
  555. #
  556. # COMPATIBITITY NOTE: the separation of *_destiny values into
  557. # D_BOUNCE, D_REJECT, D_DISCARD and D_PASS made settings $warnvirussender
  558. # and $warnspamsender only still useful with D_PASS. The combination of
  559. # D_DISCARD + $warn*sender=1 is mapped into D_BOUNCE for compatibility.
  560. # intentionally leave value -1 unassigned for compatibility
  561. sub D_REJECT () { -3 }
  562. sub D_BOUNCE () { -2 }
  563. sub D_DISCARD() { 0 }
  564. sub D_PASS () { 1 }
  565. # The following symbolic constants can be used in *destiny settings:
  566. #
  567. # D_PASS mail will pass to recipients, regardless of contents;
  568. #
  569. # D_DISCARD mail will not be delivered to its recipients, sender will NOT be
  570. # notified. Effectively we lose mail (but it will be quarantined
  571. # unless disabled).
  572. #
  573. # D_BOUNCE mail will not be delivered to its recipients, a non-delivery
  574. # notification (bounce) will be sent to the sender by amavisd-new;
  575. # Exception: bounce (DSN) will not be sent if a virus name matches
  576. # $viruses_that_fake_sender_maps, or to messages from mailing lists
  577. # (Precedence: bulk|list|junk), or for spam exceeding
  578. # spam_dsn_cutoff_level
  579. #
  580. # D_REJECT mail will not be delivered to its recipients, sender should
  581. # preferably get a reject, e.g. SMTP permanent reject response
  582. # (e.g. with milter), or non-delivery notification from MTA
  583. # (e.g. Postfix). If this is not possible (e.g. different recipients
  584. # have different tolerances to bad mail contents and not using LMTP)
  585. # amavisd-new sends a bounce by itself (same as D_BOUNCE).
  586. #
  587. # Notes:
  588. # D_REJECT and D_BOUNCE are similar, the difference is in who is responsible
  589. # for informing the sender about non-delivery, and how informative
  590. # the notification can be (amavisd-new knows more than MTA);
  591. # With D_REJECT, MTA may reject original SMTP, or send DSN (delivery status
  592. # notification, colloquially called 'bounce') - depending on MTA;
  593. # Best suited for sendmail milter, especially for spam.
  594. # With D_BOUNCE, amavisd-new (not MTA) sends DSN (can better explain the
  595. # reason for mail non-delivery but unable to reject the original
  596. # SMTP session, and is in position to suppress DSN if considered
  597. # unsuitable). Best suited for Postfix and other dual-MTA setups.
  598. $final_virus_destiny = D_DISCARD; # D_REJECT, D_BOUNCE, D_DISCARD, D_PASS
  599. $final_banned_destiny = D_BOUNCE; # D_REJECT, D_BOUNCE, D_DISCARD, D_PASS
  600. $final_spam_destiny = D_BOUNCE; # D_REJECT, D_BOUNCE, D_DISCARD, D_PASS
  601. $final_bad_header_destiny = D_PASS; # D_REJECT, D_BOUNCE, D_DISCARD, D_PASS
  602. # If you decide to pass viruses (or spam) to certain users using
  603. # @virus_lovers_maps, (or @spam_lovers_maps), or $final_virus_destiny=D_PASS
  604. # ($final_spam_destiny=D_PASS), you can set the variable $addr_extension_virus
  605. # ($addr_extension_spam) to some string, and the recipient address will have
  606. # this string appended as an address extension to the local-part of the
  607. # address. This extension can be used by final local delivery agent to place
  608. # such mail in different folders. Leave these variables undefined or empty
  609. # strings to prevent appending address extensions. Setting has no effect
  610. # on users which will not be receiving viruses (spam). Recipients which
  611. # do not match access lists in @local_domains_maps are not affected (i.e.
  612. # non-local recipients do not get address extension appended).
  613. #
  614. # LDAs usually default to stripping away address extension if no special
  615. # handling for it is specified, so having this option enabled normally
  616. # does no harm, provided the $recipients_delimiter character matches
  617. # the setting at the final MTA's local delivery agent (LDA).
  618. #
  619. # $addr_extension_virus = 'virus'; # for example
  620. # $addr_extension_spam = 'spam';
  621. # $addr_extension_banned = 'banned';
  622. # $addr_extension_bad_header = 'badh';
  623. # Delimiter between local part of the recipient address and address extension
  624. # (which can optionally be added, see variables $addr_extension_virus and
  625. # $addr_extension_spam). E.g. recipient address <user@domain.example> gets
  626. # changed to <user+virus@domain.example>.
  627. #
  628. # Delimiter should match equivalent (final) MTA delimiter setting.
  629. # (e.g. for Postfix add 'recipient_delimiter = +' to main.cf).
  630. # Setting it to an empty string or to undef disables this feature
  631. # regardless of $addr_extension_virus and $addr_extension_spam settings.
  632. # $recipient_delimiter = '+';
  633. $replace_existing_extension = 1; # true: replace ext; false: append ext
  634. # Affects matching of localpart of e-mail addresses (left of '@')
  635. # in lookups: true = case sensitive, false = case insensitive
  636. $localpart_is_case_sensitive = 0;
  637. # first match wins, more specific entries should precede general ones!
  638. # the result may be a string or a ref to a list of strings;
  639. # see also sub decompose_part()
  640. $map_full_type_to_short_type_re = Amavis::Lookup::RE->new(
  641. [qr/^empty\z/ => 'empty'],
  642. [qr/^directory\z/ => 'dir'],
  643. [qr/^can't (stat|read)\b/ => 'dat'], # file(1) diagnostics
  644. [qr/^cannot open\b/ => 'dat'], # file(1) diagnostics
  645. [qr/^ERROR: Corrupted\b/ => 'dat'], # file(1) diagnostics
  646. [qr/can't read magic file|couldn't find any magic files/ => 'dat'],
  647. [qr/^data\z/ => 'dat'],
  648. [qr/^ISO-8859.*\btext\b/ => 'txt'],
  649. [qr/^Non-ISO.*ASCII\b.*\btext\b/ => 'txt'],
  650. [qr/^Unicode\b.*\btext\b/i => 'txt'],
  651. [qr/^'diff' output text\b/ => 'txt'],
  652. [qr/^GNU message catalog\b/ => 'mo'],
  653. [qr/^PGP encrypted data\b/ => 'pgp'],
  654. [qr/^PGP armored data( signed)? message\b/ => ['pgp','pgp.asc'] ],
  655. [qr/^PGP armored\b/ => ['pgp','pgp.asc'] ],
  656. ### 'file' is a bit too trigger happy to claim something is 'mail text'
  657. # [qr/^RFC 822 mail text\b/ => 'mail'],
  658. [qr/^(ASCII|smtp|RFC 822) mail text\b/ => 'txt'],
  659. [qr/^JPEG image data\b/ =>['image','jpg'] ],
  660. [qr/^GIF image data\b/ =>['image','gif'] ],
  661. [qr/^PNG image data\b/ =>['image','png'] ],
  662. [qr/^TIFF image data\b/ =>['image','tif'] ],
  663. [qr/^PCX\b.*\bimage data\b/ =>['image','pcx'] ],
  664. [qr/^PC bitmap data\b/ =>['image','bmp'] ],
  665. [qr/^MP2\b/ =>['audio','mpa','mp2'] ],
  666. [qr/^MP3\b/ =>['audio','mpa','mp3'] ],
  667. [qr/^MPEG video stream data\b/ =>['movie','mpv'] ],
  668. [qr/^MPEG system stream data\b/ =>['movie','mpg'] ],
  669. [qr/^MPEG\b/ =>['movie','mpg'] ],
  670. [qr/^Microsoft ASF\b/ =>['movie','wmv'] ],
  671. [qr/^RIFF\b.*\bAVI\b/ =>['movie','avi'] ],
  672. [qr/^RIFF\b.*\bWAVE audio\b/ =>['audio','wav'] ],
  673. [qr/^Macromedia Flash data\b/ => 'swf'],
  674. [qr/^HTML document text\b/ => 'html'],
  675. [qr/^XML document text\b/ => 'xml'],
  676. [qr/^exported SGML document text\b/ => 'sgml'],
  677. [qr/^PostScript document text\b/ => 'ps'],
  678. [qr/^PDF document\b/ => 'pdf'],
  679. [qr/^Rich Text Format data\b/ => 'rtf'],
  680. [qr/^Microsoft Office Document\b/i => 'doc'], # OLE2: doc, ppt, xls, ...
  681. [qr/^LaTeX\b.*\bdocument text\b/ => 'lat'],
  682. [qr/^TeX DVI file\b/ => 'dvi'],
  683. [qr/\bdocument text\b/ => 'txt'],
  684. [qr/^compiled Java class data\b/ => 'java'],
  685. [qr/^MS Windows 95 Internet shortcut text\b/ => 'url'],
  686. [qr/^frozen\b/ => 'F'],
  687. [qr/^gzip compressed\b/ => 'gz'],
  688. [qr/^bzip compressed\b/ => 'bz'],
  689. [qr/^bzip2 compressed\b/ => 'bz2'],
  690. [qr/^lzop compressed\b/ => 'lzo'],
  691. [qr/^compress'd/ => 'Z'],
  692. [qr/^Zip archive\b/i => 'zip'],
  693. [qr/^RAR archive\b/i => 'rar'],
  694. [qr/^LHa.*\barchive\b/i => 'lha'], # (also known as .lzh)
  695. [qr/^ARC archive\b/i => 'arc'],
  696. [qr/^ARJ archive\b/i => 'arj'],
  697. [qr/^Zoo archive\b/i => 'zoo'],
  698. [qr/^(\S+\s+)?tar archive\b/i => 'tar'],
  699. [qr/^(\S+\s+)?cpio archive\b/i => 'cpio'],
  700. [qr/^Debian binary package\b/i => 'deb'], # standard Unix archive (ar)
  701. [qr/^current ar archive\b/i => 'a'], # standard Unix archive (ar)
  702. [qr/^RPM\b/ => 'rpm'],
  703. [qr/^(Transport Neutral Encapsulation Format|TNEF)\b/i => 'tnef'],
  704. [qr/^Microsoft cabinet file\b/ => 'cab'],
  705. [qr/^(uuencoded|xxencoded)\b/i => 'uue'],
  706. [qr/^binhex\b/i => 'hqx'],
  707. [qr/^(ASCII|text)\b/i => 'asc'],
  708. [qr/^Emacs.*byte-compiled Lisp data/i => 'asc'], # BinHex with an empty line
  709. [qr/\bscript text executable\b/ => 'txt'],
  710. [qr/^MS-DOS\b.*\bexecutable\b/ => ['exe','exe-ms'] ],
  711. [qr/^MS Windows\b.*\bexecutable\b/ => ['exe','exe-ms'] ],
  712. [qr/^PA-RISC.*\bexecutable\b/ => ['exe','exe-unix'] ],
  713. [qr/^ELF .*\bexecutable\b/ => ['exe','exe-unix'] ],
  714. [qr/^COFF format .*\bexecutable\b/ => ['exe','exe-unix'] ],
  715. [qr/^executable \(RISC System\b/ => ['exe','exe-unix'] ],
  716. [qr/^VMS\b.*\bexecutable\b/ => ['exe','exe-vms'] ],
  717. [qr/\bexecutable\b/i => 'exe'],
  718. [qr/^MS Windows\b.*\bDLL\b/ => 'dll'],
  719. [qr/\bshared object, /i => 'so'],
  720. [qr/\brelocatable, /i => 'o'],
  721. [qr/\btext\b/i => 'asc'],
  722. [qr/^/ => 'dat'], # catchall
  723. );
  724. # MS Windows PE 32-bit Intel 80386 GUI executable not relocatable
  725. # MS-DOS executable (EXE), OS/2 or MS Windows
  726. # PA-RISC1.1 executable dynamically linked
  727. # PA-RISC1.1 shared executable dynamically linked
  728. # ELF 64-bit LSB executable, Alpha (unofficial), version 1 (FreeBSD), for FreeBSD 5.0.1, dynamically linked (uses shared libs), stripped
  729. # ELF 64-bit LSB executable, Alpha (unofficial), version 1 (SYSV), for GNU/Linux 2.2.5, dynamically linked (uses shared libs), stripped
  730. # ELF 64-bit MSB executable, SPARC V9, version 1 (FreeBSD), for FreeBSD 5.0, dynamically linked (uses shared libs), stripped
  731. # ELF 64-bit MSB shared object, SPARC V9, version 1 (FreeBSD), stripped
  732. # ELF 32-bit LSB executable, Intel 80386, version 1, dynamically`
  733. # ELF 32-bit MSB executable, SPARC, version 1, dynamically linke`
  734. # COFF format alpha executable paged stripped - version 3.11-10
  735. # COFF format alpha executable paged dynamically linked stripped`
  736. # COFF format alpha demand paged executable or object module stripped - version 3.11-10
  737. # COFF format alpha paged dynamically linked not stripped shared`
  738. # executable (RISC System/6000 V3.1) or obj module
  739. # VMS VAX executable
  740. # prototypes
  741. sub Amavis::Unpackers::do_mime_decode($$);
  742. sub Amavis::Unpackers::do_ascii($$);
  743. sub Amavis::Unpackers::do_uncompress($$$);
  744. sub Amavis::Unpackers::do_gunzip($$);
  745. sub Amavis::Unpackers::do_pax_cpio($$$);
  746. sub Amavis::Unpackers::do_tar($$);
  747. sub Amavis::Unpackers::do_ar($$$);
  748. sub Amavis::Unpackers::do_unzip($$);
  749. sub Amavis::Unpackers::do_unrar($$$);
  750. sub Amavis::Unpackers::do_unarj($$$);
  751. sub Amavis::Unpackers::do_arc($$$);
  752. sub Amavis::Unpackers::do_zoo($$$);
  753. sub Amavis::Unpackers::do_lha($$$);
  754. sub Amavis::Unpackers::do_ole($$$);
  755. sub Amavis::Unpackers::do_cabextract($$$);
  756. sub Amavis::Unpackers::do_tnef($$);
  757. sub Amavis::Unpackers::do_tnef_ext($$$);
  758. sub Amavis::Unpackers::do_executable($$@);
  759. # Define alias names or shortcuts in this module to make it simpler
  760. # to call these routines from amavisd.conf
  761. *read_text = \&Amavis::Util::read_text;
  762. *read_l10n_templates = \&Amavis::Util::read_l10n_templates;
  763. *read_hash = \&Amavis::Util::read_hash;
  764. *read_array = \&Amavis::Util::read_array;
  765. *dump_hash = \&Amavis::Util::dump_hash;
  766. *dump_array = \&Amavis::Util::dump_array;
  767. *ask_daemon = \&Amavis::AV::ask_daemon;
  768. *sophos_savi = \&Amavis::AV::ask_sophos_savi;
  769. *ask_clamav = \&Amavis::AV::ask_clamav;
  770. *do_mime_decode = \&Amavis::Unpackers::do_mime_decode;
  771. *do_ascii = \&Amavis::Unpackers::do_ascii;
  772. *do_uncompress = \&Amavis::Unpackers::do_uncompress;
  773. *do_gunzip = \&Amavis::Unpackers::do_gunzip;
  774. *do_pax_cpio = \&Amavis::Unpackers::do_pax_cpio;
  775. *do_tar = \&Amavis::Unpackers::do_tar;
  776. *do_ar = \&Amavis::Unpackers::do_ar;
  777. *do_unzip = \&Amavis::Unpackers::do_unzip;
  778. *do_unrar = \&Amavis::Unpackers::do_unrar;
  779. *do_unarj = \&Amavis::Unpackers::do_unarj;
  780. *do_arc = \&Amavis::Unpackers::do_arc;
  781. *do_zoo = \&Amavis::Unpackers::do_zoo;
  782. *do_lha = \&Amavis::Unpackers::do_lha;
  783. *do_ole = \&Amavis::Unpackers::do_ole;
  784. *do_cabextract = \&Amavis::Unpackers::do_cabextract;
  785. *do_tnef_ext = \&Amavis::Unpackers::do_tnef_ext;
  786. *do_tnef = \&Amavis::Unpackers::do_tnef;
  787. *do_executable = \&Amavis::Unpackers::do_executable;
  788. sub new_RE { Amavis::Lookup::RE->new(@_) }
  789. # initialize the @decoders list
  790. sub init_decoders() {
  791. # A list of pairs or n-tuples: [short-type, code_ref, optional-args...].
  792. # Maps short types to a decoding routine, the first match wins.
  793. # Arguments beyond the first two can be program path string (or a listref of
  794. # paths to be searched) or a reference to a variable containing such a path,
  795. # which allows for lazy evaluation, making possible to assign values to
  796. # legacy configuration variables even after the assignment to @decoders.
  797. @decoders = (
  798. ['mail', \&Amavis::Unpackers::do_mime_decode],
  799. ['asc', \&Amavis::Unpackers::do_ascii],
  800. ['uue', \&Amavis::Unpackers::do_ascii],
  801. ['hqx', \&Amavis::Unpackers::do_ascii],
  802. ['ync', \&Amavis::Unpackers::do_ascii],
  803. ['F', \&Amavis::Unpackers::do_uncompress, \$unfreeze],
  804. ['Z', \&Amavis::Unpackers::do_uncompress, \$uncompress],
  805. ['gz', \&Amavis::Unpackers::do_gunzip],
  806. ['gz', \&Amavis::Unpackers::do_uncompress, \$gunzip],
  807. ['bz2', \&Amavis::Unpackers::do_uncompress, \$bunzip2],
  808. ['lzo', \&Amavis::Unpackers::do_uncompress, \$unlzop],
  809. ['rpm', \&Amavis::Unpackers::do_uncompress, \$rpm2cpio],
  810. ['cpio', \&Amavis::Unpackers::do_pax_cpio, \$pax],
  811. ['cpio', \&Amavis::Unpackers::do_pax_cpio, \$cpio],
  812. ['tar', \&Amavis::Unpackers::do_pax_cpio, \$pax],
  813. ['tar', \&Amavis::Unpackers::do_pax_cpio, \$cpio],
  814. ['tar', \&Amavis::Unpackers::do_tar],
  815. ['deb', \&Amavis::Unpackers::do_ar, \$ar],
  816. # ['a', \&Amavis::Unpackers::do_ar, \$ar], #unpacking .a seems an overkill
  817. ['zip', \&Amavis::Unpackers::do_unzip],
  818. ['rar', \&Amavis::Unpackers::do_unrar, \$unrar],
  819. ['arj', \&Amavis::Unpackers::do_unarj, \$unarj],
  820. ['arc', \&Amavis::Unpackers::do_arc, \$arc],
  821. ['zoo', \&Amavis::Unpackers::do_zoo, \$zoo],
  822. ['lha', \&Amavis::Unpackers::do_lha, \$lha],
  823. ['doc', \&Amavis::Unpackers::do_ole, \$ripole],
  824. ['cab', \&Amavis::Unpackers::do_cabextract, \$cabextract],
  825. ['tnef', \&Amavis::Unpackers::do_tnef_ext, \$tnef],
  826. ['tnef', \&Amavis::Unpackers::do_tnef],
  827. ['exe', \&Amavis::Unpackers::do_executable, \$unrar,\$lha,\$unarj],
  828. );
  829. }
  830. sub build_default_maps() {
  831. @local_domains_maps = (
  832. \%local_domains, \@local_domains_acl, \$local_domains_re);
  833. @mynetworks_maps = (\@mynetworks);
  834. @bypass_virus_checks_maps = (
  835. \%bypass_virus_checks, \@bypass_virus_checks_acl, \$bypass_virus_checks_re);
  836. @bypass_spam_checks_maps = (
  837. \%bypass_spam_checks, \@bypass_spam_checks_acl, \$bypass_spam_checks_re);
  838. @bypass_banned_checks_maps = (
  839. \%bypass_banned_checks, \@bypass_banned_checks_acl, \$bypass_banned_checks_re);
  840. @bypass_header_checks_maps = (
  841. \%bypass_header_checks, \@bypass_header_checks_acl, \$bypass_header_checks_re);
  842. @virus_lovers_maps = (
  843. \%virus_lovers, \@virus_lovers_acl, \$virus_lovers_re);
  844. @spam_lovers_maps = (
  845. \%spam_lovers, \@spam_lovers_acl, \$spam_lovers_re);
  846. @banned_files_lovers_maps = (
  847. \%banned_files_lovers, \@banned_files_lovers_acl, \$banned_files_lovers_re);
  848. @bad_header_lovers_maps = (
  849. \%bad_header_lovers, \@bad_header_lovers_acl, \$bad_header_lovers_re);
  850. @warnvirusrecip_maps = (\$warnvirusrecip);
  851. @warnbannedrecip_maps = (\$warnbannedrecip);
  852. @warnbadhrecip_maps = (\$warnbadhrecip);
  853. @newvirus_admin_maps = (\$newvirus_admin);
  854. @virus_admin_maps = (\%virus_admin, \$virus_admin);
  855. @banned_admin_maps = (\$banned_admin);
  856. @bad_header_admin_maps= (\$bad_header_admin);
  857. @spam_admin_maps = (\%spam_admin, \$spam_admin);
  858. @virus_quarantine_to_maps = (\$virus_quarantine_to);
  859. @banned_quarantine_to_maps = (\$banned_quarantine_to);
  860. @bad_header_quarantine_to_maps = (\$bad_header_quarantine_to);
  861. @spam_quarantine_to_maps = (\$spam_quarantine_to);
  862. @spam_quarantine_bysender_to_maps = (\$spam_quarantine_bysender_to);
  863. @keep_decoded_original_maps = (\$keep_decoded_original_re);
  864. @map_full_type_to_short_type_maps = (\$map_full_type_to_short_type_re);
  865. # @banned_filename_maps = ( {'.' => [$banned_filename_re]} );
  866. # @banned_filename_maps = ( {'.' => 'DEFAULT'} );#names mapped by %banned_rules
  867. @banned_filename_maps = ( 'DEFAULT' ); # same as previous, but shorter
  868. @viruses_that_fake_sender_maps = (\$viruses_that_fake_sender_re, 1);
  869. @spam_tag_level_maps = (\$sa_tag_level_deflt);
  870. @spam_tag2_level_maps = (\$sa_tag2_level_deflt);
  871. @spam_kill_level_maps = (\$sa_kill_level_deflt);
  872. @spam_dsn_cutoff_level_maps = (\$sa_dsn_cutoff_level);
  873. @spam_quarantine_cutoff_level_maps = (\$sa_quarantine_cutoff_level);
  874. @spam_modifies_subj_maps = (\$sa_spam_modifies_subj);
  875. @spam_subject_tag_maps = (\$sa_spam_subject_tag1); # note: inconsistent
  876. @spam_subject_tag2_maps = (\$sa_spam_subject_tag); # note: inconsistent
  877. @whitelist_sender_maps = (
  878. \%whitelist_sender, \@whitelist_sender_acl, \$whitelist_sender_re);
  879. @blacklist_sender_maps = (
  880. \%blacklist_sender, \@blacklist_sender_acl, \$blacklist_sender_re);
  881. @score_sender_maps = (); # new variable, no backwards compatibility needed
  882. @message_size_limit_maps = (); # new variable
  883. @addr_extension_virus_maps = (\$addr_extension_virus);
  884. @addr_extension_spam_maps = (\$addr_extension_spam);
  885. @addr_extension_banned_maps = (\$addr_extension_banned);
  886. @addr_extension_bad_header_maps = (\$addr_extension_bad_header);
  887. @debug_sender_maps = (\@debug_sender_acl);
  888. }
  889. # prepend a lookup table label object for logging purposes
  890. sub label_default_maps() {
  891. for my $varname (qw(
  892. @local_domains_maps @mynetworks_maps
  893. @bypass_virus_checks_maps @bypass_spam_checks_maps
  894. @bypass_banned_checks_maps @bypass_header_checks_maps
  895. @virus_lovers_maps @spam_lovers_maps
  896. @banned_files_lovers_maps @bad_header_lovers_maps
  897. @warnvirusrecip_maps @warnbannedrecip_maps @warnbadhrecip_maps
  898. @newvirus_admin_maps @virus_admin_maps
  899. @banned_admin_maps @bad_header_admin_maps @spam_admin_maps
  900. @virus_quarantine_to_maps
  901. @banned_quarantine_to_maps @bad_header_quarantine_to_maps
  902. @spam_quarantine_to_maps @spam_quarantine_bysender_to_maps
  903. @keep_decoded_original_maps @map_full_type_to_short_type_maps
  904. @banned_filename_maps
  905. @viruses_that_fake_sender_maps
  906. @spam_tag_level_maps @spam_tag2_level_maps @spam_kill_level_maps
  907. @spam_dsn_cutoff_level_maps @spam_quarantine_cutoff_level_maps
  908. @spam_modifies_subj_maps @spam_subject_tag_maps @spam_subject_tag2_maps
  909. @whitelist_sender_maps @blacklist_sender_maps @score_sender_maps
  910. @message_size_limit_maps
  911. @addr_extension_virus_maps @addr_extension_spam_maps
  912. @addr_extension_banned_maps @addr_extension_bad_header_maps
  913. @debug_sender_maps ))
  914. {
  915. my($g) = $varname; $g =~ s{\@}{Amavis::Conf::}; # qualified variable name
  916. my($label) = $varname; $label=~s/^\@//; $label=~s/_maps$//;
  917. { no strict 'refs';
  918. unshift(@$g, # NOTE: a symbolic reference
  919. Amavis::Lookup::Label->new($label)) if @$g; # no label if empty
  920. }
  921. }
  922. }
  923. # read and evaluate configuration files (one or more)
  924. sub read_config(@) {
  925. my(@config_files) = @_;
  926. for my $config_file (@config_files) {
  927. my($msg);
  928. my($errn) = stat($config_file) ? 0 : 0+$!;
  929. if ($errn == ENOENT) { $msg = "does not exist" }
  930. elsif ($errn) { $msg = "is inaccessible: $!" }
  931. elsif (-d _) { $msg = "is a directory" }
  932. elsif (!-f _) { $msg = "is not a regular file" }
  933. elsif ($> && -o _) { $msg = "is owned by EUID $>, should be owned by root"}
  934. elsif ($> && -w _) { $msg = "is writable by EUID $>, EGID $)" }
  935. if (defined $msg) { die "Config file \"$config_file\" $msg," }
  936. $! = 0;
  937. if (defined(do $config_file)) {}
  938. elsif ($@ ne '') { die "Error in config file \"$config_file\": $@" }
  939. elsif ($! != 0) { die "Error reading config file \"$config_file\": $!" }
  940. }
  941. $daemon_chroot_dir = ''
  942. if !defined $daemon_chroot_dir || $daemon_chroot_dir eq '/';
  943. # provide some sensible defaults for essential settings (post-defaults)
  944. $TEMPBASE = $MYHOME if !defined $TEMPBASE;
  945. $helpers_home = $MYHOME if !defined $helpers_home;
  946. $db_home = "$MYHOME/db" if !defined $db_home;
  947. $lock_file = "$MYHOME/amavisd.lock" if !defined $lock_file;
  948. $pid_file = "$MYHOME/amavisd.pid" if !defined $pid_file;
  949. $X_HEADER_TAG = 'X-Virus-Scanned' if !defined $X_HEADER_TAG;
  950. $X_HEADER_LINE= "$myproduct_name at $mydomain" if !defined $X_HEADER_LINE;
  951. $gunzip = "$gzip -d" if !defined $gunzip && $gzip ne '';
  952. $bunzip2 = "$bzip2 -d" if !defined $bunzip2 && $bzip2 ne '';
  953. $unlzop = "$lzop -d" if !defined $unlzop && $lzop ne '';
  954. my($pname) = "\"Content-filter at $myhostname\"";
  955. $hdrfrom_notify_sender = "$pname <postmaster\@$myhostname>"
  956. if !defined $hdrfrom_notify_sender;
  957. $hdrfrom_notify_recip = $mailfrom_notify_recip ne ''
  958. ? "$pname <$mailfrom_notify_recip>"
  959. : $hdrfrom_notify_sender if !defined $hdrfrom_notify_recip;
  960. $hdrfrom_notify_admin = $mailfrom_notify_admin ne ''
  961. ? "$pname <$mailfrom_notify_admin>"
  962. : $hdrfrom_notify_sender if !defined $hdrfrom_notify_admin;
  963. $hdrfrom_notify_spamadmin = $mailfrom_notify_spamadmin ne ''
  964. ? "$pname <$mailfrom_notify_spamadmin>"
  965. : $hdrfrom_notify_sender if !defined $hdrfrom_notify_spamadmin;
  966. # compatibility with deprecated $warn*sender and old *_destiny values
  967. # map old values <0, =0, >0 into D_REJECT/D_BOUNCE, D_DISCARD, D_PASS
  968. for ($final_virus_destiny, $final_banned_destiny, $final_spam_destiny) {
  969. if ($_ > 0) { $_ = D_PASS }
  970. elsif ($_ < 0 && $_ != D_BOUNCE && $_ != D_REJECT) { # compatibility
  971. # favour Reject with sendmail milter, Bounce with others
  972. $_ = c('forward_method') eq '' ? D_REJECT : D_BOUNCE;
  973. }
  974. }
  975. if ($final_virus_destiny == D_DISCARD && c('warnvirussender') )
  976. { $final_virus_destiny = D_BOUNCE }
  977. if ($final_spam_destiny == D_DISCARD && c('warnspamsender') )
  978. { $final_spam_destiny = D_BOUNCE }
  979. if ($final_banned_destiny == D_DISCARD && c('warnbannedsender') )
  980. { $final_banned_destiny = D_BOUNCE }
  981. if ($final_bad_header_destiny == D_DISCARD && c('warnbadhsender') )
  982. { $final_bad_header_destiny = D_BOUNCE }
  983. if (!%banned_rules) {
  984. # an associative array mapping a rule name
  985. # to a single 'banned names/types' lookup table
  986. %banned_rules = ('DEFAULT'=>$banned_filename_re); # backwards compatibile
  987. }
  988. }
  989. 1;
  990. #
  991. package Amavis::Lock;
  992. use strict;
  993. use re 'taint';
  994. BEGIN {
  995. use Exporter ();
  996. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  997. $VERSION = '2.043';
  998. @ISA = qw(Exporter);
  999. @EXPORT = qw(&lock &unlock);
  1000. }
  1001. use Fcntl qw(LOCK_SH LOCK_EX LOCK_UN);
  1002. use subs @EXPORT;
  1003. sub lock($) {
  1004. my($file_handle) = @_;
  1005. flock($file_handle, LOCK_EX) or die "Can't lock $file_handle: $!";
  1006. # NOTE: a lock is on a file, not on a file handle
  1007. }
  1008. sub unlock($) {
  1009. my($file_handle) = @_;
  1010. flock($file_handle, LOCK_UN) or die "Can't unlock $file_handle: $!";
  1011. }
  1012. 1;
  1013. #
  1014. package Amavis::Log;
  1015. use strict;
  1016. use re 'taint';
  1017. BEGIN {
  1018. use Exporter ();
  1019. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  1020. $VERSION = '2.043';
  1021. @ISA = qw(Exporter);
  1022. @EXPORT_OK = qw(&init &write_log &open_log &close_log &log_fd);
  1023. }
  1024. use subs @EXPORT_OK;
  1025. use POSIX qw(locale_h strftime);
  1026. use Unix::Syslog qw(:macros :subs);
  1027. use IO::File ();
  1028. use File::Basename;
  1029. BEGIN {
  1030. import Amavis::Conf qw(:platform $myversion $myhostname $daemon_user);
  1031. import Amavis::Lock;
  1032. }
  1033. use vars qw($loghandle); # log file handle
  1034. use vars qw($myname);
  1035. use vars qw($syslog_facility $syslog_priority %syslog_priority);
  1036. use vars qw($log_to_stderr $do_syslog $logfile);
  1037. sub init($$$$) {
  1038. my($syslog_level);
  1039. ($log_to_stderr, $do_syslog, $syslog_level, $logfile) = @_;
  1040. $myname = $0;
  1041. if ($syslog_level =~ /^\s*([a-z0-9]+)\.([a-z0-9]+)\s*\z/i) {
  1042. $syslog_facility = eval("LOG_\U$1");
  1043. $syslog_priority = eval("LOG_\U$2");
  1044. }
  1045. $syslog_facility = LOG_DAEMON if $syslog_facility !~ /^\d+\z/;
  1046. $syslog_priority = LOG_WARNING if $syslog_priority !~ /^\d+\z/;
  1047. open_log();
  1048. if (!$do_syslog && $logfile eq '')
  1049. { print STDERR "Logging to STDERR (no \$LOGFILE and no \$DO_SYSLOG)\n" }
  1050. my($msg) = "starting. $myname at $myhostname $myversion";
  1051. $msg .= ", eol=\"$eol\"" if $eol ne "\n";
  1052. $msg .= ", Unicode aware" if $unicode_aware;
  1053. $msg .= ", LC_ALL=$ENV{LC_ALL}" if $ENV{LC_ALL} ne '';
  1054. $msg .= ", LC_TYPE=$ENV{LC_TYPE}" if $ENV{LC_TYPE} ne '';
  1055. $msg .= ", LC_CTYPE=$ENV{LC_CTYPE}" if $ENV{LC_CTYPE} ne '';
  1056. $msg .= ", LANG=$ENV{LANG}" if $ENV{LANG} ne '';
  1057. write_log(0, $msg, undef);
  1058. }
  1059. sub open_log() {
  1060. # don't bother to skip opening the log even if $log_to_stderr (debug) is true
  1061. if ($do_syslog) {
  1062. openlog('amavis', LOG_PID | LOG_NDELAY, $syslog_facility);
  1063. } elsif ($logfile ne '') {
  1064. $loghandle = IO::File->new($logfile,'>>')
  1065. or die "Failed to open log file $logfile: $!";
  1066. $loghandle->autoflush(1);
  1067. if ($> == 0) {
  1068. my($uid) = $daemon_user=~/^(\d+)$/ ? $1 : (getpwnam($daemon_user))[2];
  1069. if ($uid) {
  1070. chown($uid,-1,$logfile)
  1071. or die "Can't chown logfile $logfile to $uid: $!";
  1072. }
  1073. }
  1074. }
  1075. }
  1076. sub close_log() {
  1077. if ($do_syslog) {
  1078. closelog();
  1079. } elsif (defined($loghandle) && $logfile ne '') {
  1080. $loghandle->close or die "Error closing log file $logfile: $!";
  1081. $loghandle = undef;
  1082. }
  1083. }
  1084. # Log either to syslog or to a file
  1085. sub write_log($$$) {
  1086. my($level,$errmsg,$am_id) = @_;
  1087. $am_id = !defined $am_id ? '' : "($am_id) ";
  1088. $errmsg = Amavis::Util::sanitize_str($errmsg);
  1089. # my($old_locale) = POSIX::setlocale(LC_TIME,"C"); # English dates required!
  1090. # if (length($errmsg) > 2000) { # crop at some arbitrary limit (< LINE_MAX)
  1091. # $errmsg = substr($errmsg,0,2000) . "...";
  1092. # }
  1093. if ($do_syslog && !$log_to_stderr) {
  1094. my($prio) = $syslog_priority; # never go below this priority level
  1095. # syslog priorities: DEBUG, INFO, NOTICE, WARNING, ERR, CRIT, ALERT, EMERG
  1096. if ($level <= -3) { $prio = LOG_CRIT if $prio > LOG_CRIT }
  1097. elsif ($level <= -2) { $prio = LOG_ERR if $prio > LOG_ERR }
  1098. elsif ($level <= -1) { $prio = LOG_WARNING if $prio > LOG_WARNING }
  1099. elsif ($level <= 0) { $prio = LOG_NOTICE if $prio > LOG_NOTICE }
  1100. elsif ($level <= 2) { $prio = LOG_INFO if $prio > LOG_INFO }
  1101. else { $prio = LOG_DEBUG if $prio > LOG_DEBUG }
  1102. my($pre) = '';
  1103. my($logline_size) = 980; # less than (1023 - prefix)
  1104. while (length($am_id)+length($pre)+length($errmsg) > $logline_size) {
  1105. my($avail) = $logline_size - length($am_id . $pre . "...");
  1106. syslog($prio, "%s", $am_id . $pre . substr($errmsg,0,$avail) . "...");
  1107. $pre = "...";
  1108. $errmsg = substr($errmsg, $avail);
  1109. }
  1110. syslog($prio, "%s", $am_id . $pre . $errmsg);
  1111. } else {
  1112. my($prefix) = sprintf("%s %s %s[%s]: ", # prepare syslog-alike prefix
  1113. strftime("%b %e %H:%M:%S",localtime), $myhostname, $myname, $$);
  1114. if (defined $loghandle && !$log_to_stderr) {
  1115. lock($loghandle);
  1116. seek($loghandle,0,2) or die "Can't position log file to its tail: $!";
  1117. $loghandle->print($prefix, $am_id, $errmsg, $eol)
  1118. or die "Error writing to log file: $!";
  1119. unlock($loghandle);
  1120. } else {
  1121. print STDERR $prefix, $am_id, $errmsg, $eol
  1122. or die "Error writing to STDERR: $!";
  1123. }
  1124. }
  1125. # POSIX::setlocale(LC_TIME, $old_locale);
  1126. }
  1127. sub log_fd() {
  1128. $log_to_stderr ? fileno(STDERR)
  1129. : $do_syslog ? undef # how to obtain fd on syslog?
  1130. : defined $loghandle ? $loghandle->fileno : fileno(STDERR);
  1131. }
  1132. 1;
  1133. #
  1134. package Amavis::Timing;
  1135. use strict;
  1136. use re 'taint';
  1137. BEGIN {
  1138. use Exporter ();
  1139. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  1140. $VERSION = '2.043';
  1141. @ISA = qw(Exporter);
  1142. @EXPORT_OK = qw(&init &section_time &report &get_time_so_far);
  1143. }
  1144. use subs @EXPORT_OK;
  1145. use Time::HiRes 1.49 ();
  1146. use vars qw(@timing);
  1147. # clear array @timing and enter start time
  1148. sub init() {
  1149. @timing = (); section_time('init');
  1150. }
  1151. # enter current time reading into array @timing
  1152. sub section_time($) {
  1153. push(@timing,shift,Time::HiRes::time);
  1154. }
  1155. # returns a string - a report of elapsed time by section
  1156. sub report() {
  1157. section_time('rundown');
  1158. my($notneeded, $t0) = (shift(@timing), shift(@timing));
  1159. my($total) = $t0 <= 0 ? 0 : $timing[$#timing] - $t0;
  1160. if ($total < 0.0000001) { $total = 0.0000001 }
  1161. my(@sections); my($t00) = $t0;
  1162. while (@timing) {
  1163. my($section, $t) = (shift(@timing), shift(@timing));
  1164. my($dt) = $t <= $t0 ? 0 : $t-$t0; # handle possible clock jumps
  1165. my($dt_c) = $t <= $t00 ? 0 : $t-$t00; # handle possible clock jumps
  1166. my($dtp) = $dt >= $total ? 100 : $dt*100.0/$total; # this event
  1167. my($dtp_c) = $dt_c >= $total ? 100 : $dt_c*100.0/$total; # cumulative
  1168. push(@sections, sprintf("%s: %.0f (%.0f%%)%.0f",
  1169. $section, $dt*1000, $dtp, $dtp_c));
  1170. $t0 = $t;
  1171. }
  1172. sprintf("TIMING [total %.0f ms] - %s", $total * 1000, join(", ",@sections));
  1173. }
  1174. # returns value in seconds of elapsed time for processing of this mail so far
  1175. sub get_time_so_far() {
  1176. my($notneeded, $t0) = @timing;
  1177. my($total) = $t0 <= 0 ? 0 : Time::HiRes::time - $t0;
  1178. $total < 0 ? 0 : $total;
  1179. }
  1180. use vars qw($t_was_busy $t_busy_cum $t_idle_cum $t0);
  1181. sub idle_proc(@) {
  1182. my($t1) = Time::HiRes::time;
  1183. if (defined $t0) {
  1184. ($t_was_busy ? $t_busy_cum : $t_idle_cum) += $t1 - $t0;
  1185. Amavis::Util::ll(5) && Amavis::Util::do_log(5,
  1186. sprintf("idle_proc, @_: was %s, %.1f ms, total idle %.3f s, busy %.3f s",
  1187. $t_was_busy ? "busy" : "idle", 1000 * ($t1 - $t0),
  1188. $t_idle_cum, $t_busy_cum));
  1189. }
  1190. $t0 = $t1;
  1191. }
  1192. sub go_idle(@) {
  1193. if ($t_was_busy) { idle_proc(@_); $t_was_busy = 0 }
  1194. }
  1195. sub go_busy(@) {
  1196. if (!$t_was_busy) { idle_proc(@_); $t_was_busy = 1 }
  1197. }
  1198. sub report_load() {
  1199. return if $t_busy_cum + $t_idle_cum <= 0;
  1200. Amavis::Util::do_log(3, sprintf(
  1201. "load: %.0f %%, total idle %.3f s, busy %.3f s",
  1202. 100*$t_busy_cum / ($t_busy_cum + $t_idle_cum), $t_idle_cum, $t_busy_cum));
  1203. }
  1204. 1;
  1205. #
  1206. package Amavis::Util;
  1207. use strict;
  1208. use re 'taint';
  1209. BEGIN {
  1210. use Exporter ();
  1211. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  1212. $VERSION = '2.043';
  1213. @ISA = qw(Exporter);
  1214. @EXPORT_OK = qw(&untaint &min &max &safe_encode &safe_decode &q_encode
  1215. &snmp_count &snmp_counters_init &snmp_counters_get
  1216. &am_id &new_am_id &ll &do_log &debug_oneshot
  1217. &add_entropy &fetch_entropy &generate_mail_id
  1218. &retcode &exit_status_str &prolong_timer
  1219. &sanitize_str &fmt_struct &strip_tempdir &rmdir_recursively
  1220. &read_text &read_l10n_templates &read_hash &read_array
  1221. &dump_hash &dump_array &run_command &run_command_consumer);
  1222. }
  1223. use subs @EXPORT_OK;
  1224. use POSIX qw(WIFEXITED WIFSIGNALED WIFSTOPPED
  1225. WEXITSTATUS WTERMSIG WSTOPSIG);
  1226. use Errno qw(ENOENT EACCES);
  1227. use Digest::MD5 2.22; # need 'clone' method
  1228. # use Encode; # Perl 5.8 UTF-8 support
  1229. BEGIN {
  1230. import Amavis::Conf qw(:platform $DEBUG c cr ca);
  1231. import Amavis::Log qw(write_log open_log close_log log_fd);
  1232. import Amavis::Timing qw(section_time);
  1233. }
  1234. # Return untainted copy of a string (argument can be a string or a string ref)
  1235. sub untaint($) {
  1236. no re 'taint';
  1237. my($str);
  1238. if (defined($_[0])) {
  1239. local($1); # avoid Perl taint bug: tainted global $1 propagates taintedness
  1240. $str = $1 if (ref($_[0]) ? ${$_[0]} : $_[0]) =~ /^(.*)\z/s;
  1241. }
  1242. $str;
  1243. }
  1244. # Returns the smallest defined number from the list, or undef
  1245. sub min(@) {
  1246. my($r) = @_ == 1 && ref($_[0]) ? $_[0] : \@_; # accept list, or a list ref
  1247. my($m); for (@$r) { $m = $_ if defined $_ && (!defined $m || $_ < $m) }
  1248. $m;
  1249. }
  1250. # Returns the largest defined number from the list, or undef
  1251. sub max(@) {
  1252. my($r) = @_ == 1 && ref($_[0]) ? $_[0] : \@_; # accept list, or a list ref
  1253. my($m); for (@$r) { $m = $_ if defined $_ && (!defined $m || $_ > $m) }
  1254. $m;
  1255. }
  1256. # A wrapper for Encode::encode, avoiding a bug in Perl 5.8.0 which causes
  1257. # Encode::encode to loop and fill memory when given a tainted string
  1258. #
  1259. # hmh@d.o : in Debian's 5.8.4-2, trying to restore the taintedness
  1260. # actually causes perl to somehow lose track of the encoding and it
  1261. # completely breaks this sub. OTOH, perl does loop eating up memory
  1262. # on tainted strings, so we will have to lose taint state for now.
  1263. sub safe_encode($$;$) {
  1264. if (!$unicode_aware) { $_[1] } # just return the second argument
  1265. else {
  1266. my($encoding,$str,$check) = @_;
  1267. $check = 0 if !defined($check);
  1268. $str = untaint(\$str);
  1269. return Encode::encode($encoding, $str, $check); # reattach taintedness
  1270. # # taintedness of the string, with UTF-8 flag unconditionally off
  1271. # my($taint) = Encode::encode('ascii',substr($str,0,0));
  1272. # $taint . Encode::encode($encoding,untaint($str),$check); # preserve taint
  1273. }
  1274. }
  1275. sub safe_decode($$;$) {
  1276. if (!$unicode_aware) { $_[1] } # just return the second argument
  1277. else {
  1278. my($encoding,$str,$check) = @_;
  1279. $check = 0 if !defined($check);
  1280. my($taint) = substr($str,0,0); # taintedness of the string
  1281. $taint . Encode::decode($encoding,untaint($str),$check); # preserve taint
  1282. }
  1283. }
  1284. # Do the Q-encoding manually, the MIME::Words::encode_mimeword does not
  1285. # encode spaces and does not limit to 75 ch, which violates the RFC 2047
  1286. sub q_encode($$$) {
  1287. my($octets,$encoding,$charset) = @_;
  1288. my($prefix) = '=?' . $charset . '?' . $encoding . '?';
  1289. my($suffix) = '?='; local($1,$2,$3);
  1290. # FWS | utext (= NO-WS-CTL|rest of US-ASCII)
  1291. $octets =~ /^ ( [\001-\011\013\014\016-\177]* [ \t] )? (.*?)
  1292. ( [ \t] [\001-\011\013\014\016-\177]* )? \z/sx;
  1293. my($head,$rest,$tail) = ($1,$2,$3);
  1294. # Q-encode $rest according to RFC 2047
  1295. # more restricted than =?_ so that it may be used in 'phrase'
  1296. $rest =~ s{([^ 0-9a-zA-Z!*/+-])}{sprintf('=%02X',ord($1))}egs;
  1297. $rest =~ tr/ /_/; # turn spaces into _ (rfc2047 allows it)
  1298. my($s) = $head; my($len) = 75 - (length($prefix)+length($suffix)) - 2;
  1299. while ($rest ne '') {
  1300. $s .= ' ' if $s !~ /[ \t]\z/; # encoded words must be separated by FWS
  1301. $rest =~ /^ ( .{0,$len} [^=] (?: [^=] | \z ) ) (.*) \z/sx;
  1302. $s .= $prefix.$1.$suffix; $rest = $2;
  1303. }
  1304. $s.$tail;
  1305. }
  1306. # Set or get Amavis internal message id.
  1307. # This message id performs a similar function as queue-id in MTA responses.
  1308. # It may only be used in generating text part of SMTP responses,
  1309. # or in generating log entries. It is only unique within a limited timespan.
  1310. use vars qw($amavis_task_id); # internal message id (accessible via &am_id)
  1311. sub am_id(;$) {
  1312. if (@_) { # set, if argument present
  1313. $amavis_task_id = shift;
  1314. $0 = "amavisd ($amavis_task_id)";
  1315. }
  1316. $amavis_task_id; # return current value
  1317. }
  1318. sub new_am_id($;$$) {
  1319. my($str, $cnt, $seq) = @_;
  1320. my($id);
  1321. $id = defined $str ? $str : sprintf("%05d", $$);
  1322. $id .= sprintf("-%02d", $cnt) if defined $cnt;
  1323. $id .= "-$seq" if defined $seq && $seq > 1;
  1324. am_id($id);
  1325. }
  1326. use vars qw($entropy); # MD5 ctx (128 bits, 32 hex digits or 22 base64 chars)
  1327. sub add_entropy(@) {
  1328. $entropy = Digest::MD5->new if !defined $entropy;
  1329. my($s) = join(",", map {!defined($_) ? 'U' : ref eq 'ARRAY' ? @$_ : $_} @_);
  1330. # do_log(5,"add_entropy: ".$s);
  1331. $entropy->add($s);
  1332. }
  1333. sub fetch_entropy() {
  1334. $entropy->clone->b64digest;
  1335. }
  1336. # generate a reasonably unique (long-term) id based on collected entropy.
  1337. # The result is a pair of (mostly public) mail_id, and a secret id,
  1338. # where mail_id == b64(md5(b64(secret))). The secret id could be used to
  1339. # authorize releasing quarantined mail. Both the mail_id and secret are
  1340. # 12-char strings of characters [A-Za-z0-9+-], with an additional restriction
  1341. # for mail_id which must begin and end with an alphanumeric character.
  1342. sub generate_mail_id() {
  1343. my($secret_id,$id,$rest);
  1344. for (my $j=0; $j<100; $j++) { # provide some sanity loop limit just in case
  1345. # take 72 bits from entropy accum. to produce a secret id, leave 56 bits
  1346. local($1,$2); $entropy->clone->b64digest =~ /^(.{12})(.*)\z/s;
  1347. ($secret_id,$rest) = ($1,$2); $secret_id =~ tr{/}{-}; # [A-Za-z0-9+-]
  1348. # mail_id computed as md5(secret_id), rely on unidirectionality of md5
  1349. $id = Digest::MD5->new->add($secret_id)->b64digest; # md5(b64(secret_id))
  1350. last if $id =~ /^[A-Za-z0-9].{10}[A-Za-z0-9]/s; # starts&ends with alfnum
  1351. add_entropy($j); # retry on less than 7% of cases
  1352. do_log(5,"generate_mail_id retry: $id");
  1353. }
  1354. # start with a fresh entropy accumulator, wiping out traces of secret id
  1355. $entropy = undef;
  1356. add_entropy($rest); # carry over unused portion of old entropy accumulator
  1357. add_entropy($id); # mix-in the full mail_id before chopping it to 12 chars
  1358. $id = substr($id,0,12); $id =~ tr{/}{-};
  1359. ($id,$secret_id);
  1360. }
  1361. use vars qw(@counter_names);
  1362. # elements may be counter names (increment is 1), or pairs: [name,increment]
  1363. sub snmp_counters_init() { @counter_names = () }
  1364. sub snmp_count(@) { push(@counter_names, @_) }
  1365. sub snmp_counters_get() { \@counter_names }
  1366. use vars qw($debug_oneshot);
  1367. sub debug_oneshot(;$$) {
  1368. if (@_) {
  1369. my($new_debug_oneshot) = shift;
  1370. if (($new_debug_oneshot ? 1 : 0) != ($debug_oneshot ? 1 : 0)) {
  1371. do_log(0, "DEBUG_ONESHOT: TURNED ".($new_debug_oneshot ? "ON" : "OFF"));
  1372. do_log(0, shift) if @_; # caller-provided extra log entry, usually
  1373. # the one that caused debug_oneshot call
  1374. }
  1375. $debug_oneshot = $new_debug_oneshot;
  1376. }
  1377. $debug_oneshot;
  1378. }
  1379. # is a message log level below the current log level?
  1380. sub ll($) {
  1381. my($level) = @_;
  1382. $level = 0 if $level > 0 && ($DEBUG || $debug_oneshot);
  1383. my($current_log_level) = c('log_level');
  1384. $current_log_level = 0 if !defined($current_log_level);
  1385. $level <= $current_log_level;
  1386. }
  1387. # write log entry
  1388. sub do_log($$) {
  1389. my($level, $errmsg) = @_;
  1390. if (ll($level)) {
  1391. $level = 0 if $level > 0 && ($DEBUG || $debug_oneshot);
  1392. write_log($level, $errmsg, am_id());
  1393. }
  1394. }
  1395. sub retcode($) { # (this subroutine is being phased out)
  1396. my $code = shift;
  1397. return WEXITSTATUS($code) if WIFEXITED($code);
  1398. return 128 + WTERMSIG($code) if WIFSIGNALED($code);
  1399. return 255;
  1400. }
  1401. # map process termination status number to a string, and append optional
  1402. # user error mesage, returning the resulting string
  1403. sub exit_status_str($;$) {
  1404. my($stat,$err) = @_; my($str);
  1405. if (WIFEXITED($stat)) {
  1406. $str = sprintf("exit %d", WEXITSTATUS($stat));
  1407. } elsif (WIFSTOPPED($stat)) {
  1408. $str = sprintf("stopped, signal %d", WSTOPSIG($stat));
  1409. } else {
  1410. $str = sprintf("DIED on signal %d (%04x)", WTERMSIG($stat),$stat);
  1411. }
  1412. $str .= ', '.$err if defined $err && $err ne '';
  1413. $str;
  1414. }
  1415. sub prolong_timer($;$) {
  1416. my($which_section, $child_remaining_time) = @_;
  1417. if (!defined($child_remaining_time)) {
  1418. $child_remaining_time = alarm(0); # check how much time is left
  1419. }
  1420. do_log(4, "prolong_timer after $which_section: "
  1421. . "remaining time = $child_remaining_time s");
  1422. $child_remaining_time = 60 if $child_remaining_time < 60;
  1423. alarm($child_remaining_time); # restart/prolong the timer
  1424. }
  1425. # Mostly for debugging and reporting purposes:
  1426. # Convert nonprintable characters in the argument
  1427. # to \[rnftbe], or \octal code, and '\' to '\\',
  1428. # and Unicode characters to \x{xxxx}, returning the sanitized string.
  1429. sub sanitize_str {
  1430. my($str, $keep_eol) = @_;
  1431. my(%map) = ("\r" => '\\r', "\n" => '\\n', "\f" => '\\f', "\t" => '\\t',
  1432. "\b" => '\\b', "\e" => '\\e', "\\" => '\\\\');
  1433. if ($keep_eol) {
  1434. $str =~ s/([^\012\040-\133\135-\176])/ # and \240-\376 ?
  1435. exists($map{$1}) ? $map{$1} :
  1436. sprintf(ord($1)>255 ? '\\x{%04x}' : '\\%03o', ord($1))/eg;
  1437. } else {
  1438. $str =~ s/([^\040-\133\135-\176])/ # and \240-\376 ?
  1439. exists($map{$1}) ? $map{$1} :
  1440. sprintf(ord($1)>255 ? '\\x{%04x}' : '\\%03o', ord($1))/eg;
  1441. }
  1442. $str;
  1443. }
  1444. # pretty-print a structure for logging purposes: returns a string
  1445. sub fmt_struct($) {
  1446. my($arg) = @_;
  1447. !defined($arg) ? 'undef' : !ref($arg) ? '"'.$arg.'"' :
  1448. ref($arg) eq 'ARRAY' ? '['.join(',',map {fmt_struct($_)} @$arg).']' : $arg;
  1449. };
  1450. # Checks tempdir after being cleaned.
  1451. # It may only contain subdirectory 'parts' and file email.txt, nothing else.
  1452. #
  1453. sub check_tempdir($) {
  1454. my($dir) = shift;
  1455. local(*DIR); opendir(DIR,$dir) or die "Can't open directory $dir: $!";
  1456. eval {
  1457. undef $!, my($f);
  1458. while (defined($f = readdir(DIR))) {
  1459. if (!-d ("$dir/$f")) {
  1460. die "Unexpected file $dir/$f" if $f ne 'email.txt';
  1461. } elsif ($f eq '.' || $f eq '..' || $f eq 'parts') {
  1462. } else {
  1463. die "Unexpected subdirectory $dir/$f";
  1464. }
  1465. }
  1466. # $!==0 or die "Error reading directory $dir: $!";
  1467. };
  1468. closedir(DIR) or die "Error closing directory $dir: $!";
  1469. if ($@ ne '') { chomp($@); die "check_tempdir: $@\n" }
  1470. 1;
  1471. }
  1472. # Remove all files and subdirectories from the temporary directory, leaving
  1473. # only the directory itself, file email.txt, and empty subdirectory ./parts .
  1474. # Leaving directories for reuse represents an important saving in time,
  1475. # as directory creation + deletion is quite an expensive operation,
  1476. # requiring atomic file system operation, including flushing buffers to disk.
  1477. #
  1478. sub strip_tempdir($) {
  1479. my($dir) = shift;
  1480. do_log(4, "strip_tempdir: $dir");
  1481. my($errn) = lstat("$dir/parts") ? 0 : 0+$!;
  1482. if ($errn == ENOENT) {} # fine, no such directory
  1483. elsif ($errn != 0) { die "strip_tempdir: error accessing $dir/parts: $!" }
  1484. elsif ( -l _) { die "strip_tempdir: $dir/parts is a symbolic link" }
  1485. elsif (!-d _) { die "strip_tempdir: $dir/parts is not a directory" }
  1486. else { rmdir_recursively("$dir/parts", 1) }
  1487. # All done. Check for any remains in the top directory just in case
  1488. check_tempdir($dir);
  1489. 1;
  1490. }
  1491. #
  1492. # Removes a directory, along with its contents
  1493. sub rmdir_recursively($;$); # prototype
  1494. sub rmdir_recursively($;$) {
  1495. my($dir, $exclude_itself) = @_; my($cnt) = 0;
  1496. do_log(4,"rmdir_recursively: $dir, excl=$exclude_itself");
  1497. local(*DIR); my($errn) = opendir(DIR,$dir) ? 0 : 0+$!;
  1498. if ($errn == ENOENT) { die "Directory $dir does not exist," }
  1499. elsif ($errn == EACCES) { # relax protection on directory, then try again
  1500. do_log(3,"rmdir_recursively: enabling read access to directory $dir");
  1501. chmod(0750,$dir) or die "Can't change protection-1 on dir $dir: $!";
  1502. $errn = opendir(DIR,$dir) ? 0 : 0+$!; # try again
  1503. }
  1504. if ($errn) { die "Can't open directory $dir: $!" }
  1505. my(@dirfiles) = readdir(DIR); # must avoid modifying dir. while traversing it
  1506. closedir(DIR) or die "Error closing directory $dir: $!";
  1507. for my $f (@dirfiles) {
  1508. my($fname) = "$dir/$f";
  1509. $errn = lstat($fname) ? 0 : 0+$!;
  1510. if ($errn == ENOENT) { die "File \"$fname\" does not exist" }
  1511. elsif ($errn == EACCES) { # relax protection on the directory and retry
  1512. do_log(3,"rmdir_recursively: enabling access to files in dir $dir");
  1513. chmod(0750,$dir) or die "Can't change protection-2 on dir $dir: $!";
  1514. $errn = lstat($fname) ? 0 : 0+$!; # try again
  1515. }
  1516. if ($errn) { die "File \"$fname\" inaccessible: $!" }
  1517. next if ($f eq '.' || $f eq '..') && -d _;
  1518. if (-d _) { rmdir_recursively(untaint($fname), 0) }
  1519. else {
  1520. $cnt++;
  1521. if (unlink(untaint($fname))) { # ok
  1522. } else { # relax protection on the directory, then try again
  1523. do_log(3,"rmdir_recursively: enabling write access to dir $dir");
  1524. my($what) = -l _ ? 'symlink' :-d _ ? 'directory' :'non-regular file';
  1525. chmod(0750,$dir) or die "Can't change protection-3 on dir $dir: $!";
  1526. unlink(untaint($fname)) or die "Can't remove $what $fname: $!";
  1527. }
  1528. }
  1529. }
  1530. section_time("unlink-$cnt-files");
  1531. if (!$exclude_itself) {
  1532. rmdir($dir) or die "rmdir_recursively: Can't remove directory $dir: $!";
  1533. section_time('rmdir');
  1534. }
  1535. 1;
  1536. }
  1537. # read a multiline string from a file - may be called from amavisd.conf
  1538. sub read_text($;$) {
  1539. my($filename, $encoding) = @_;
  1540. my($inp) = IO::File->new;
  1541. $inp->open($filename,'<') or die "Can't open file $filename for reading: $!";
  1542. if ($unicode_aware && $encoding ne '') {
  1543. binmode($inp, ":encoding($encoding)")
  1544. or die "Can't set :encoding($encoding) on file $filename: $!";
  1545. }
  1546. my($str) = ''; # must not be undef, work around a Perl UTF8 bug
  1547. my($nbytes,$buff);
  1548. while (($nbytes=$inp->read($buff,16384)) > 0) { $str .= $buff }
  1549. defined $nbytes or die "Error reading from $filename: $!";
  1550. $inp->close or die "Error closing $filename: $!";
  1551. $str;
  1552. }
  1553. # attempt to read all user-visible replies from a l10n dir
  1554. # This function auto-fills $notify_sender_templ, $notify_virus_sender_templ,
  1555. # $notify_virus_admin_templ, $notify_virus_recips_templ,
  1556. # $notify_spam_sender_templ and $notify_spam_admin_templ from files named
  1557. # template-dsn.txt, template-virus-sender.txt, template-virus-admin.txt,
  1558. # template-virus-recipient.txt, template-spam-sender.txt,
  1559. # template-spam-admin.txt. If this is available, it uses the charset
  1560. # file to do automatic charset conversion. Used by the Debian distribution.
  1561. sub read_l10n_templates($;$) {
  1562. my($dir) = @_;
  1563. if (@_ > 1) # compatibility with Debian
  1564. { my($l10nlang, $l10nbase) = @_; $dir = "$l10nbase/$l10nlang" }
  1565. my($file_chset) = Amavis::Util::read_text("$dir/charset");
  1566. if ($file_chset =~ m{^(?:#[^\n]*\n)*([^./\n\s]+)(\s*[#\n].*)?$}s) {
  1567. $file_chset = untaint($1);
  1568. } else {
  1569. die "Invalid charset $file_chset\n";
  1570. }
  1571. $Amavis::Conf::notify_sender_templ =
  1572. Amavis::Util::read_text("$dir/template-dsn.txt", $file_chset);
  1573. $Amavis::Conf::notify_virus_sender_templ =
  1574. Amavis::Util::read_text("$dir/template-virus-sender.txt", $file_chset);
  1575. $Amavis::Conf::notify_virus_admin_templ =
  1576. Amavis::Util::read_text("$dir/template-virus-admin.txt", $file_chset);
  1577. $Amavis::Conf::notify_virus_recips_templ =
  1578. Amavis::Util::read_text("$dir/template-virus-recipient.txt", $file_chset);
  1579. $Amavis::Conf::notify_spam_sender_templ =
  1580. Amavis::Util::read_text("$dir/template-spam-sender.txt", $file_chset);
  1581. $Amavis::Conf::notify_spam_admin_templ =
  1582. Amavis::Util::read_text("$dir/template-spam-admin.txt", $file_chset);
  1583. }
  1584. #use CDB_File;
  1585. #sub tie_hash($$) {
  1586. # my($hashref, $filename) = @_;
  1587. # CDB_File::create(%$hashref, $filename, "$filename.tmp$$")
  1588. # or die "Can't create cdb $filename: $!";
  1589. # my($cdb) = tie(%$hashref,'CDB_File',$filename)
  1590. # or die "Tie to $filename failed: $!";
  1591. # $hashref;
  1592. #}
  1593. # read a lookup associative array (Perl hash) from a file - may be called
  1594. # from amavisd.conf
  1595. #
  1596. # Format: one key per line, anything from '#' to the end of line
  1597. # is considered a comment, but '#' within correctly quoted rfc2821
  1598. # addresses is not treated as a comment (e.g. a hash sign within
  1599. # "strange # \"foo\" address"@example.com is part of the string).
  1600. # Lines may contain a pair: key value, separated by whitespace, or key only,
  1601. # in which case a value 1 is implied. Trailing whitespace is discarded,
  1602. # empty lines (containing only whitespace and comment) are ignored.
  1603. # Addresses (lefthand-side) are converted from rfc2821-quoted form
  1604. # into internal (raw) form and inserted as keys into a given hash.
  1605. # NOTE: the format is partly compatible with Postfix maps (not aliases):
  1606. # no continuation lines are honoured, Postfix maps do not allow
  1607. # rfc2821-quoted addresses containing whitespace, Postfix only allows
  1608. # comments starting at the beginning of a line.
  1609. #
  1610. # The $hashref argument is returned for convenience, so that one can do
  1611. # for example:
  1612. # $per_recip_whitelist_sender_lookup_tables = {
  1613. # '.my1.example.com' => read_hash({},'/var/amavis/my1-example-com.wl'),
  1614. # '.my2.example.com' => read_hash({},'/var/amavis/my2-example-com.wl') }
  1615. # or even simpler:
  1616. # $per_recip_whitelist_sender_lookup_tables = {
  1617. # '.my1.example.com' => read_hash('/var/amavis/my1-example-com.wl'),
  1618. # '.my2.example.com' => read_hash('/var/amavis/my2-example-com.wl') }
  1619. #
  1620. sub read_hash(@) {
  1621. unshift(@_,{}) if !ref $_[0]; # first argument is optional, defaults to {}
  1622. my($hashref, $filename, $keep_case) = @_;
  1623. my($lpcs) = c('localpart_is_case_sensitive');
  1624. my($inp) = IO::File->new;
  1625. $inp->open($filename,'<') or die "Can't open file $filename for reading: $!";
  1626. my($ln);
  1627. for (undef $!; defined($ln=$inp->getline); undef $!) {
  1628. chomp($ln);
  1629. # carefully handle comments, '#' within "" does not count as a comment
  1630. my($lhs) = ''; my($rhs) = ''; my($at_rhs) = 0;
  1631. for my $t ( $ln =~ /\G ( " (?: \\. | [^"\\] )* " |
  1632. [^#" \t]+ | [ \t]+ | . )/gcsx) {
  1633. last if $t eq '#';
  1634. if (!$at_rhs && $t =~ /^[ \t]+\z/) { $at_rhs = 1 }
  1635. else { ($at_rhs ? $rhs : $lhs) .= $t }
  1636. }
  1637. $rhs =~ s/[ \t]+\z//; # trim trailing whitespace
  1638. next if $lhs eq '' && $rhs eq '';
  1639. my($addr) = Amavis::rfc2821_2822_Tools::unquote_rfc2821_local($lhs);
  1640. my($localpart,$domain) = Amavis::rfc2821_2822_Tools::split_address($addr);
  1641. $localpart = lc($localpart) if !$lpcs;
  1642. $addr = $localpart . lc($domain);
  1643. $hashref->{$addr} = $rhs eq '' ? 1 : $rhs;
  1644. # do_log(5, "read_hash: address: <$addr>: ".$hashref->{$addr});
  1645. }
  1646. defined $ln || $!==0 or die "Error reading from $filename: $!";
  1647. $inp->close or die "Error closing $filename: $!";
  1648. $hashref;
  1649. }
  1650. sub read_array(@) {
  1651. unshift(@_,[]) if !ref $_[0]; # first argument is optional, defaults to []
  1652. my($arrref, $filename, $keep_case) = @_;
  1653. my($inp) = IO::File->new;
  1654. $inp->open($filename,'<') or die "Can't open file $filename for reading: $!";
  1655. my($ln);
  1656. for (undef $!; defined($ln=$inp->getline); undef $!) {
  1657. chomp($ln); my($lhs) = '';
  1658. # carefully handle comments, '#' within "" does not count as a comment
  1659. for my $t ( $ln =~ /\G ( " (?: \\. | [^"\\] )* " |
  1660. [^#" \t]+ | [ \t]+ | . )/gcsx) {
  1661. last if $t eq '#';
  1662. $lhs .= $t;
  1663. }
  1664. $lhs =~ s/[ \t]+\z//; # trim trailing whitespace
  1665. push(@$arrref, Amavis::rfc2821_2822_Tools::unquote_rfc2821_local($lhs))
  1666. if $lhs ne '';
  1667. }
  1668. defined $ln || $!==0 or die "Error reading from $filename: $!";
  1669. $inp->close or die "Error closing $filename: $!";
  1670. $arrref;
  1671. }
  1672. sub dump_hash($) {
  1673. my($hr) = @_;
  1674. do_log(0, sprintf("dump_hash: %s => %s", $_,$hr->{$_})) for (sort keys %$hr);
  1675. }
  1676. sub dump_array($) {
  1677. my($ar) = @_;
  1678. do_log(0, sprintf("dump_array: %s", $_)) for @$ar;
  1679. }
  1680. # Run specified command as a subprocess. Return a file handle open for
  1681. sub run_command($$@) {
  1682. my($stdin_from, $stderr_to, $cmd, @args) = @_;
  1683. my($cmd_text) = join(' ', $cmd, @args);
  1684. $stdin_from = '/dev/null' if $stdin_from eq '';
  1685. $stderr_to = '/dev/null' if defined($stderr_to) && $stderr_to eq '';
  1686. my($msg) = join(' ', $cmd, @args, "<$stdin_from",
  1687. $stderr_to eq '' ? () : "2>$stderr_to");
  1688. # $^F == 2 or do_log(-1,"run_command: SYSTEM_FD_MAX not 2: %d", $^F);
  1689. my($pid); my($proc_fh) = IO::File->new;
  1690. eval {
  1691. $pid = $proc_fh->open('-|'); 1; # fork, catching errors
  1692. } or do {
  1693. my($eval_stat) = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  1694. die "run_command (open pipe): $eval_stat";
  1695. };
  1696. defined($pid) or die "run_command: can't fork: $!";
  1697. if (!$pid) { # child
  1698. alarm(0); my($interrupt) = '';
  1699. my($h1) = sub { $interrupt = $_[0] };
  1700. my($h2) = sub { die "Received signal ".$_[0] };
  1701. @SIG{qw(INT HUP TERM TSTP QUIT USR1 USR2)} = ($h1) x 7;
  1702. eval { # die must be caught, otherwise we end up with two running daemons
  1703. local(@SIG{qw(INT HUP TERM TSTP QUIT USR1 USR2)}) = ($h2) x 7;
  1704. if ($interrupt ne '') { my($i) = $interrupt; $interrupt = ''; die $i }
  1705. # use Devel::Symdump ();
  1706. # my($dumpobj) = Devel::Symdump->rnew;
  1707. # for my $k ($dumpobj->ios) {
  1708. # no strict 'refs'; my($fn) = fileno($k);
  1709. # if (!defined($fn)) { do_log(2, "not open %s", $k) }
  1710. # elsif ($fn == 1 || $fn == 2) { do_log(2, "KEEP %s, fileno=%s",$k,$fn) }
  1711. # else { $! = 0;
  1712. # close(*{$k}{IO}) and do_log(2, "DID CLOSE %s (fileno=%s)", $k,$fn);
  1713. # }
  1714. # }
  1715. release_parent_resources();
  1716. open_on_specific_fd(0,$stdin_from,&POSIX::O_RDONLY,0);
  1717. open_on_specific_fd(2,$stderr_to,&POSIX::O_WRONLY,0) if $stderr_to ne '';
  1718. # eval { close_log() }; # may have been closed by open_on_specific_fd
  1719. # BEWARE of Perl older that 5.6.0: sockets and pipes were not FD_CLOEXEC
  1720. exec {$cmd} ($cmd,@args);
  1721. die "run_command: failed to exec $cmd_text: $!";
  1722. };
  1723. my($err) = $@ ne '' ? $@ : "errno=$!"; chomp $err;
  1724. eval {
  1725. local(@SIG{qw(INT HUP TERM TSTP QUIT USR1 USR2)}) = ($h2) x 7;
  1726. if ($interrupt ne '') { my($i) = $interrupt; $interrupt = ''; die $i }
  1727. open_log(); # oops, exec failed, we will need logging after all...
  1728. # we're in trouble if stderr was attached to a terminal, but no longer is
  1729. do_log(-1,sprintf("run_command: child process [%s]: %s", $$,$err));
  1730. };
  1731. { no warnings;
  1732. POSIX::_exit(8); # avoid END and destructor processing
  1733. kill('KILL',$$); exit 1; # still kicking? die!
  1734. }
  1735. }
  1736. # parent
  1737. ll(5) && do_log(5,sprintf("run_command: [%s] %s", $pid,$msg));
  1738. binmode($proc_fh) or die "Can't set pipe to binmode: $!"; # dflt Perl 5.8.1
  1739. ($proc_fh, $pid); # return pipe file handle to the subprocess and its PID
  1740. }
  1741. # POSIX::open a file or dup an existing fd (Perl open syntax), with a
  1742. # requirement that it gets opened on a prescribed file descriptor $fd_target;
  1743. # this subroutine is usually called from a forked process prior to exec
  1744. sub open_on_specific_fd($$$$) {
  1745. my($fd_target,$fname,$flags,$mode) = @_;
  1746. my($fd_got); # fd directy given as argument, or obtained from POSIX::open
  1747. my($logging_safe) = 0;
  1748. if (ll(5)) {
  1749. # crude attempt to prevent a forked process from writing log records
  1750. # to its parent process on STDOUT or STDERR
  1751. my($log_fd) = log_fd();
  1752. $logging_safe = 1 if !defined($log_fd) || $log_fd > 2;
  1753. }
  1754. local($1);
  1755. if ($fname =~ /^&=?(\d+)\z/) { $fd_got = $1 } # fd directly specified
  1756. my($flags_displayed) = $flags == &POSIX::O_RDONLY ? '<'
  1757. : $flags == &POSIX::O_WRONLY ? '>' : $flags;
  1758. if (!defined($fd_got) || $fd_got != $fd_target) {
  1759. # close whatever is on a target descriptor but don't shoot self in the foot
  1760. # with Net::Server <= 0.90 fd0 was main::stdin, but no longer is in 0.91
  1761. do_log(5, sprintf("open_on_specific_fd: target fd%s closing, to become %s %s",
  1762. $fd_target,$flags_displayed,$fname)) if $logging_safe;
  1763. # it pays off to close explicitly, with some luck open will get a target fd
  1764. POSIX::close($fd_target); # ignore error, we may have just closed a log
  1765. }
  1766. if (!defined($fd_got)) { # file name was given, not a descriptor
  1767. $fd_got = POSIX::open($fname,$flags,$mode);
  1768. defined $fd_got or die "Can't open $fname: $!";
  1769. $fd_got = 0 + $fd_got; # turn into numeric, avoid: "0 but true"
  1770. }
  1771. if ($fd_got != $fd_target) { # dup, ensuring we get a specified descriptor
  1772. eval { # we may have been left without a log file descriptor, must not die
  1773. do_log(5, sprintf("open_on_specific_fd: target fd%s dup2 from fd%s %s %s",
  1774. $fd_target,$fd_got,$flags_displayed,$fname)) if $logging_safe;
  1775. };
  1776. # POSIX mandates we got the lowest fd available (but some kernels have
  1777. # bugs), let's be explicit that we require a specified file descriptor
  1778. defined POSIX::dup2($fd_got,$fd_target)
  1779. or die "Can't dup2 from $fd_got to $fd_target: $!";
  1780. if ($fd_got > 2) { # let's get rid of the original fd, unless 0,1,2
  1781. my($err); defined POSIX::close($fd_got) or $err = $!;
  1782. $err = defined $err ? ": $err" : '';
  1783. eval { # we may have been left without a log file descriptor, don't die
  1784. do_log(5, sprintf("open_on_specific_fd: source fd%s closed%s",
  1785. $fd_got,$err)) if $logging_safe;
  1786. };
  1787. }
  1788. }
  1789. $fd_got;
  1790. }
  1791. sub release_parent_resources() {
  1792. $Amavis::sql_dataset_conn_lookups->dbh_inactive(1)
  1793. if $Amavis::sql_dataset_conn_lookups;
  1794. $Amavis::sql_dataset_conn_storage->dbh_inactive(1)
  1795. if $Amavis::sql_dataset_conn_storage;
  1796. # undef $Amavis::sql_dataset_conn_lookups;
  1797. # undef $Amavis::sql_dataset_conn_storage;
  1798. # undef $Amavis::body_digest_cache; undef $Amavis::snmp_db;
  1799. # undef $Amavis::db_env;
  1800. }
  1801. # WRITING to the subprocess. Use IO::Handle to ensure the subprocess
  1802. # will be automatically reclaimed in case of failure.
  1803. #
  1804. sub run_command_consumer($$@) {
  1805. my($stdout_to, $stderr_to, $cmd, @args) = @_;
  1806. my($cmd_text) = join(' ', $cmd, @args);
  1807. $stdout_to = '/dev/null' if $stdout_to eq '';
  1808. my($msg) = join(' ', $cmd, @args, ">$stdout_to");
  1809. $msg .= " 2>$stderr_to" if $stderr_to ne '';
  1810. my($pid); my($proc_fh) = IO::File->new;
  1811. eval { $pid = $proc_fh->open('|-') }; # fork, catching errors
  1812. if ($@ ne '') { chomp($@); die "run_command_consumer (open pipe): $@" }
  1813. defined($pid) or die "run_command_consumer: can't fork: $!";
  1814. if (!$pid) { # child
  1815. eval { # must not use die in forked process, or we end up with
  1816. # two running daemons! Close unneeded files.
  1817. # $sql_dataset_conn_lookups->dbh_inactive(1) if $sql_dataset_conn_lookups;
  1818. # $sql_dataset_conn_storage->dbh_inactive(1) if $sql_dataset_conn_storage;
  1819. # $sql_dataset_conn_lookups = $sql_dataset_conn_storage = undef;
  1820. close_log();
  1821. close(main::stderr) or die "Error closing main::stderr: $!";
  1822. close(main::stdout) or die "Error closing main::stdout: $!";
  1823. close(main::STDOUT) or die "Error closing main::STDOUT: $!";
  1824. open(STDOUT, ">$stdout_to")
  1825. or die "Can't reopen STDOUT on $stdout_to: $!";
  1826. fileno(STDOUT) == 1
  1827. or die ("run_command_consumer: STDOUT not fd1: ".fileno(STDOUT));
  1828. if ($stderr_to ne '') {
  1829. close(STDERR) or die "Error closing STDERR: $!";
  1830. open(STDERR, ">$stderr_to")
  1831. or die "Can't open STDERR to $stderr_to: $!";
  1832. fileno(STDERR) == 2
  1833. or die ("run_command_consumer: STDERR not fd2: ".fileno(STDERR));
  1834. }
  1835. # BEWARE of Perl older that 5.6.0: sockets and pipes were not FD_CLOEXEC
  1836. { no warnings;
  1837. exec {$cmd} ($cmd,@args) or die "Failed to exec $cmd_text: $!";
  1838. }
  1839. };
  1840. my($err) = $@; chomp($err);
  1841. eval {
  1842. open_log(); # oops, exec failed, we will need logging after all...
  1843. do_log(-2,"run_command_consumer: child process [$$]: $err\n");
  1844. };
  1845. { no warnings;
  1846. POSIX::_exit(1); # avoid END and destructor processing
  1847. kill('KILL',$$) # still kicking? die!
  1848. or do_log(-3,"run_command_consumer: TROUBLE - Panic1, can't die: $!");
  1849. do_log(-3,"run_command_consumer: TROUBLE - Panic2, can't die");
  1850. exit 1; # better safe than sorry
  1851. # NOTREACHED
  1852. }
  1853. }
  1854. # parent
  1855. do_log(5,"run_command_consumer: [$pid] $msg");
  1856. binmode($proc_fh) or die "Can't set pipe to binmode: $!"; # dflt Perl 5.8.1
  1857. ($proc_fh, $pid); # return pipe file handle to the subprocess and its PID
  1858. }
  1859. 1;
  1860. #
  1861. package Amavis::rfc2821_2822_Tools;
  1862. use strict;
  1863. use re 'taint';
  1864. BEGIN {
  1865. use Exporter ();
  1866. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  1867. $VERSION = '2.043';
  1868. @ISA = qw(Exporter);
  1869. @EXPORT = qw(
  1870. &iso8601_timestamp &iso8601_utc_timestamp &rfc2822_timestamp
  1871. &received_line &parse_received
  1872. &fish_out_ip_from_received &split_address &split_localpart &make_query_keys
  1873. &quote_rfc2821_local &qquote_rfc2821_local &unquote_rfc2821_local
  1874. &one_response_for_all
  1875. &EX_OK &EX_NOUSER &EX_UNAVAILABLE &EX_TEMPFAIL &EX_NOPERM);
  1876. }
  1877. use subs @EXPORT;
  1878. use POSIX qw(locale_h strftime);
  1879. BEGIN {
  1880. eval { require 'sysexits.ph' }; # try to use the installed version
  1881. # define the most important constants if undefined
  1882. do { sub EX_OK() {0} } unless defined(&EX_OK);
  1883. do { sub EX_NOUSER() {67} } unless defined(&EX_NOUSER);
  1884. do { sub EX_UNAVAILABLE() {69} } unless defined(&EX_UNAVAILABLE);
  1885. do { sub EX_TEMPFAIL() {75} } unless defined(&EX_TEMPFAIL);
  1886. do { sub EX_NOPERM() {77} } unless defined(&EX_NOPERM);
  1887. }
  1888. BEGIN {
  1889. import Amavis::Conf qw(:platform $myhostname c cr ca);
  1890. import Amavis::Util qw(ll do_log);
  1891. }
  1892. # Given a Unix time, return the local time zone offset at that time
  1893. # as a string +HHMM or -HHMM, appropriate for the RFC2822 date format.
  1894. # Works also for non-full-hour zone offsets, and on systems where strftime
  1895. # can not return TZ offset as a number; (c) Mark Martinec, GPL
  1896. #
  1897. sub get_zone_offset($) {
  1898. my($t) = @_;
  1899. my($d) = 0; # local zone offset in seconds
  1900. for (1..3) { # match the date (with a safety loop limit just in case)
  1901. my($r) = sprintf("%04d%02d%02d", (localtime($t))[5, 4, 3]) cmp
  1902. sprintf("%04d%02d%02d", (gmtime($t + $d))[5, 4, 3]);
  1903. if ($r == 0) { last } else { $d += $r * 24 * 3600 }
  1904. }
  1905. my($sl,$su) = (0,0);
  1906. for ((localtime($t))[2,1,0]) { $sl = $sl * 60 + $_ }
  1907. for ((gmtime($t + $d))[2,1,0]) { $su = $su * 60 + $_ }
  1908. $d += $sl - $su; # add HMS difference (in seconds)
  1909. my($sign) = $d >= 0 ? '+' : '-';
  1910. $d = -$d if $d < 0;
  1911. $d = int(($d + 30) / 60.0); # give minutes, rounded
  1912. sprintf("%s%02d%02d", $sign, int($d / 60), $d % 60);
  1913. }
  1914. # Given a Unix numeric time (seconds since 1970-01-01T00:00Z),
  1915. # provide date-time timestamp (local time) as specified in ISO 8601 (EN 28601)
  1916. #
  1917. sub iso8601_timestamp($;$$) {
  1918. my($t,$suppress_zone,$separator) = @_;
  1919. # can't use %z because some systems do not support it (is treated as %Z)
  1920. my($s) = strftime("%Y%m%dT%H%M%S", localtime($t));
  1921. $s =~ s/T/$separator/ if defined $separator;
  1922. $s .= get_zone_offset($t) unless $suppress_zone;
  1923. $s;
  1924. }
  1925. # Given a Unix numeric time (seconds since 1970-01-01T00:00Z),
  1926. # provide date-time timestamp (UTC) as specified in ISO 8601 (EN 28601)
  1927. #
  1928. sub iso8601_utc_timestamp($;$$) {
  1929. my($t,$suppress_zone,$separator) = @_;
  1930. my($s) = strftime("%Y%m%dT%H%M%S", gmtime($t));
  1931. $s =~ s/T/$separator/ if defined $separator;
  1932. $s .= 'Z' unless $suppress_zone;
  1933. $s;
  1934. }
  1935. # Given a Unix time, provide date-time timestamp as specified in RFC 2822
  1936. # (local time), to be used in header fields such as 'Date:' and 'Received:'
  1937. #
  1938. sub rfc2822_timestamp($) {
  1939. my($t) = @_;
  1940. my(@lt) = localtime($t);
  1941. # can't use %z because some systems do not support it (is treated as %Z)
  1942. # my($old_locale) = POSIX::setlocale(LC_TIME,"C"); # English dates required!
  1943. my($zone_name) = strftime("%Z",@lt);
  1944. my($s) = strftime("%a, %e %b %Y %H:%M:%S ", @lt);
  1945. $s .= get_zone_offset($t);
  1946. $s .= " (" . $zone_name . ")" if $zone_name !~ /^\s*\z/;
  1947. # POSIX::setlocale(LC_TIME, $old_locale); # restore the locale
  1948. $s;
  1949. }
  1950. sub received_line($$$$) {
  1951. my($conn, $msginfo, $id, $folded) = @_;
  1952. my($smtp_proto, $recips) = ($conn->smtp_proto, $msginfo->recips);
  1953. my($client_ip) = $conn->client_ip;
  1954. if ($client_ip =~ /:/ && $client_ip !~ /^IPv6:/i) {
  1955. $client_ip = 'IPv6:' . $client_ip;
  1956. }
  1957. my($s) = sprintf("from %s%s\n by %s%s (amavisd-new, %s)",
  1958. ($conn->smtp_helo eq '' ? 'unknown' : $conn->smtp_helo),
  1959. ($client_ip eq '' ? '' : " ([$client_ip])"),
  1960. c('localhost_name'),
  1961. ($conn->socket_ip eq '' ? ''
  1962. : sprintf(" (%s [%s])", $myhostname, $conn->socket_ip) ),
  1963. ($conn->socket_port eq '' ? 'unix socket' : "port ".$conn->socket_port) );
  1964. $s .= "\n with $smtp_proto" if $smtp_proto=~/^(ES|S|L)MTPS?A?\z/i; # rfc3848
  1965. $s .= "\n id $id" if $id ne '';
  1966. # do not disclose recipients if more than one
  1967. $s .= "\n for " . qquote_rfc2821_local(@$recips) if @$recips == 1;
  1968. $s .= ";\n " . rfc2822_timestamp($msginfo->rx_time);
  1969. $s =~ s/\n//g if !$folded;
  1970. $s;
  1971. }
  1972. sub parse_received($) {
  1973. my($received) = @_;
  1974. local($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11);
  1975. $received =~ s/\n([ \t])/$1/g; # unfold
  1976. $received =~ s/[\n\r]//g; # delete remaining newlines if any
  1977. my(%fields);
  1978. while ($received =~ m{\G\s*
  1979. ( \b(from|by) \s+ ( (?: \[ (?: \\. | [^\]\\] )* \] | [^;\s\[] )+ )
  1980. (?: \s* \( (?: ( [^\s\[]+ ) \s+ )?
  1981. \[ ( (?: \\. | [^\]\\] )* ) \] \s*
  1982. \) )?
  1983. (?: .*? ) (?= \(|;|\z|\b(?:from|by|via|with|id|for)\b ) # junk
  1984. | \b(via|with|id|for) \s+
  1985. ( (?: " (?: \\. | [^"\\] )* "
  1986. | \[ (?: \\. | [^\]\\] )* \]
  1987. | \\. | [0-9a-z]+ | . # greedy words avoid deep recursion
  1988. )+? (?= \(|;|\z|\b(?:from|by|via|with|id|for)\b ) )
  1989. | (;) \s* ( .*? ) \s* \z # time
  1990. | (.*?) (?= \(|;|\z|\b(?:from|by|via|with|id|for)\b ) # junk
  1991. ) ( (?: \s+ | (?: \( (?: \\. | [^)\\] )* \) ) )* ) }xgcsi)
  1992. {
  1993. my($v1, $v2, $v3, $comment) = ('') x 4;
  1994. my($item, $field) = ($1, lc($2 || $6 || $8));
  1995. $field = '' if !defined($field); # mute a warning about uninit. value
  1996. if ($field eq 'from' || $field eq 'by') {
  1997. ($v1, $v2, $v3, $comment) = ($3, $4, $5, $11);
  1998. } elsif ($field eq ';') { # time
  1999. ($v1, $comment) = ($9, $11);
  2000. } elsif (!defined($10) || $10 eq '') { # via|with|id|for
  2001. ($v1, $comment) = ($7, $11);
  2002. } else { # junk
  2003. ($v1, $comment) = ($10, $11);
  2004. }
  2005. $comment =~ s/^\s+//;
  2006. $comment =~ s/\s+\z//;
  2007. $item =~ s/^\Q$field\E\s*//i;
  2008. if (!exists $fields{$field}) {
  2009. $fields{$field} = [$item, $v1, $v2, $v3, $comment];
  2010. ll(5) && do_log(5, sprintf("parse_received: %s = %s/%s/%s/%s",
  2011. map { !defined($_) ? '' : length($_) <= 50 ? $_
  2012. : substr($_,0,50)."..." }
  2013. ($field, @{$fields{$field}}) )) if $field ne '';
  2014. }
  2015. }
  2016. \%fields;
  2017. }
  2018. sub fish_out_ip_from_received($) {
  2019. my($received) = @_;
  2020. my($ip);
  2021. my($fields_ref) = parse_received($received);
  2022. if (defined $fields_ref && exists $fields_ref->{'from'}) {
  2023. my($item, $v1, $v2, $v3, $comment) = @{$fields_ref->{'from'}};
  2024. for (map {defined $_ ? $_ : ''} ($v3, $v2, $v1, $comment, $item)) {
  2025. if (/ \[ (\d{1,3} (?: \. \d{1,3}){3}) \] /x) {
  2026. $ip = $1; last;
  2027. } elsif (/ (\d{1,3} (?: \. \d{1,3}){3}) (?!\d) /x) {
  2028. $ip = $1; last;
  2029. } elsif (/ \[ (IPv6:)? ( ([0-9a-zA-Z]* : ){2,} [0-9a-zA-Z:.]* ) \] /xi) {
  2030. $ip = $2; last;
  2031. }
  2032. }
  2033. do_log(5, "fish_out_ip_from_received: $ip, $item");
  2034. }
  2035. !defined($ip) ? undef : $ip; # undef need not be tainted
  2036. }
  2037. # Splits unquoted fully qualified e-mail address, or an address
  2038. # with missing domain part. Returns a pair: (localpart, domain).
  2039. # The domain part (if nonempty) includes the '@' as the first character.
  2040. # If the syntax is badly broken, everything ends up as the localpart.
  2041. # The domain part can be an address literal, as specified by rfc2822.
  2042. # Does not handle explicit route paths.
  2043. #
  2044. sub split_address($) {
  2045. my($mailbox) = @_;
  2046. $mailbox =~ /^ (.*?) ( \@ (?: \[ (?: \\. | [^\]\\] )* \]
  2047. | [^@"<>\[\]\\\s] )*
  2048. ) \z/xs ? ($1, $2) : ($mailbox, '');
  2049. }
  2050. # split_localpart() splits localpart of an e-mail address at the first
  2051. # occurrence of the address extension delimiter character. (based on
  2052. # equivalent routine in Postfix)
  2053. #
  2054. # Reserved addresses are not split: postmaster, mailer-daemon,
  2055. # double-bounce. Addresses that begin with owner-, or addresses
  2056. # that end in -request are not split when the owner_request_special
  2057. # parameter is set.
  2058. sub split_localpart($$) {
  2059. my($localpart, $delimiter) = @_;
  2060. my($owner_request_special) = 1; # configurable ???
  2061. my($extension);
  2062. if ($localpart =~ /^(postmaster|mailer-daemon|double-bounce)\z/i) {
  2063. # do not split these, regardless of what the delimiter is
  2064. } elsif ($delimiter eq '-' && $owner_request_special &&
  2065. $localpart =~ /^owner-.|.-request\z/si) {
  2066. # don't split owner-foo or foo-request
  2067. } elsif ($localpart =~ /^(.+?)\Q$delimiter\E(.*)\z/s) {
  2068. ($localpart, $extension) = ($1, $2);
  2069. # do not split the address if the result would have a null localpart
  2070. }
  2071. ($localpart, $extension);
  2072. }
  2073. # For a given email address (e.g. for User+Foo@sub.exAMPLE.CoM)
  2074. # prepare and return a list of lookup keys in the following order:
  2075. # User+Foo@sub.exAMPLE.COM (as-is, no lowercasing)
  2076. # user+foo@sub.example.com
  2077. # user@sub.example.com (only if $recipient_delimiter nonempty)
  2078. # user+foo(@) (only if $include_bare_user)
  2079. # user(@) (only if $include_bare_user and $recipient_delimiter nonempty)
  2080. # (@)sub.example.com
  2081. # (@).sub.example.com
  2082. # (@).example.com
  2083. # (@).com
  2084. # (@).
  2085. # Note about (@): if $at_with_user is true the user-only keys (without domain)
  2086. # get an '@' character appended (e.g. 'user+foo@'). Usual for lookup_hash.
  2087. # If $at_with_user is false the domain-only (without localpart) keys
  2088. # get a '@' prepended (e.g. '@.example.com'). Usual for SQL and LDAP lookups.
  2089. #
  2090. # The domain part is lowercased in all but the first item in the resulting
  2091. # list; the localpart is lowercased iff $localpart_is_case_sensitive is true.
  2092. #
  2093. sub make_query_keys($$$) {
  2094. my($addr,$at_with_user,$include_bare_user) = @_;
  2095. my($localpart,$domain) = split_address($addr); $domain = lc($domain);
  2096. my($saved_full_localpart) = $localpart;
  2097. $localpart = lc($localpart) if !c('localpart_is_case_sensitive');
  2098. # chop off leading @, and trailing dots
  2099. $domain = $1 if $domain =~ /^\@?(.*?)\.*\z/s;
  2100. my($extension); my($delim) = c('recipient_delimiter');
  2101. if ($delim ne '') {
  2102. ($localpart,$extension) = split_localpart($localpart,$delim);
  2103. }
  2104. $extension = '' if !defined($extension); # mute warnings
  2105. my($append_to_user,$prepend_to_domain) = $at_with_user ? ('@','') : ('','@');
  2106. my(@keys); # a list of query keys
  2107. push(@keys, $addr); # as is
  2108. push(@keys, $localpart.$delim.$extension.'@'.$domain)
  2109. if $extension ne ''; # user+foo@example.com
  2110. push(@keys, $localpart.'@'.$domain); # user@example.com
  2111. if ($include_bare_user) { # typically enabled for local users only
  2112. push(@keys, $localpart.$delim.$extension.$append_to_user)
  2113. if $extension ne ''; # user+foo(@)
  2114. push(@keys, $localpart.$append_to_user); # user(@)
  2115. }
  2116. push(@keys, $prepend_to_domain.$domain); # (@)sub.example.com
  2117. if ($domain =~ /\[/) { # don't split address literals
  2118. push(@keys, $prepend_to_domain.'.'); # (@).
  2119. } else {
  2120. my(@dkeys); my($d) = $domain;
  2121. for (;;) { # (@).sub.example.com (@).example.com (@).com (@).
  2122. push(@dkeys, $prepend_to_domain.'.'.$d);
  2123. last if $d eq '';
  2124. $d = ($d =~ /^([^.]*)\.(.*)\z/s) ? $2 : '';
  2125. }
  2126. if (@dkeys > 10) { @dkeys = @dkeys[$#dkeys-9 .. $#dkeys] } # sanity limit
  2127. push(@keys,@dkeys);
  2128. }
  2129. my($keys_ref) = []; # remove duplicates
  2130. for my $k (@keys) { push(@$keys_ref,$k) if !grep {$k eq $_} @$keys_ref }
  2131. ll(5) && do_log(5,"query_keys: ".join(', ',@$keys_ref));
  2132. # the rhs replacement strings are similar to what would be obtained
  2133. # by lookup_re() given the following regular expression:
  2134. # /^( ( ( [^@]*? ) ( \Q$delim\E [^@]* )? ) (?: \@ (.*) ) )$/xs
  2135. my($rhs) = [ # a list of right-hand side replacement strings
  2136. $addr, # $1 = User+Foo@Sub.Example.COM
  2137. $saved_full_localpart, # $2 = User+Foo
  2138. $localpart, # $3 = user
  2139. $delim.$extension, # $4 = +foo
  2140. $domain, # $5 = sub.example.com
  2141. ];
  2142. ($keys_ref, $rhs);
  2143. }
  2144. # quote_rfc2821_local() quotes the local part of a mailbox address
  2145. # (given in internal (unquoted) form), and returns external (quoted)
  2146. # mailbox address, as per rfc2821.
  2147. #
  2148. # Internal (unquoted) form is used internally by amavisd-new and other mail sw,
  2149. # external (quoted) form is used in SMTP commands and message headers.
  2150. #
  2151. # The quote_rfc2821_local() conversion is necessary because addresses
  2152. # we get from certain MTAs are raw, with stripped-off quoting.
  2153. # To re-insert message back via SMTP, the local-part of the address needs
  2154. # to be quoted again if it contains reserved characters or otherwise
  2155. # does not obey the dot-atom syntax, as specified in rfc2821.
  2156. # Failing to do that gets us into trouble: amavis accepts message from MTA,
  2157. # but is unable to hand it back to MTA after checking, receiving
  2158. # '501 Bad address syntax' with every attempt.
  2159. #
  2160. sub quote_rfc2821_local($) {
  2161. my($mailbox) = @_;
  2162. # atext: any character except controls, SP, and specials (rfc2821/rfc2822)
  2163. my($atext) = "a-zA-Z0-9!#\$%&'*/=?^_`{|}~+-";
  2164. # my($specials) = '()<>\[\]\\\\@:;,."';
  2165. my($localpart,$domain) = split_address($mailbox);
  2166. if ($localpart !~ /^[$atext]+(\.[$atext]+)*\z/so) { # not dot-atom
  2167. $localpart =~ s/(["\\])/\\$1/g; # quoted-pair
  2168. # special case: Postfix hates ""@domain but is not so harsh on @domain
  2169. $localpart = '"'.$localpart.'"' if $localpart ne ''; # make it a qcontent
  2170. }
  2171. $domain = '' if $domain eq '@'; # strip off empty domain entirely
  2172. $localpart . $domain;
  2173. }
  2174. # wraps the result of quote_rfc2821_local into angle brackets <...> ;
  2175. # If given a list, it returns a list (possibly converted to
  2176. # comma-separated scalar if invoked in scalar context), quoting each element;
  2177. #
  2178. sub qquote_rfc2821_local(@) {
  2179. my(@r) = map { $_ eq '' ? '<>' : ('<' . quote_rfc2821_local($_) . '>') } @_;
  2180. wantarray ? @r : join(', ', @r);
  2181. }
  2182. # unquote_rfc2821_local() strips away the quoting from the local part
  2183. # of an external (quoted) mailbox address, and returns internal (unquoted)
  2184. # mailbox address, as per rfc2821.
  2185. #
  2186. # Internal (unquoted) form is used internally by amavisd-new and other mail sw,
  2187. # external (quoted) form is used in SMTP commands and message headers.
  2188. #
  2189. sub unquote_rfc2821_local($) {
  2190. my($mailbox) = @_;
  2191. # the angle-bracket stripping is not really a duty of this subroutine,
  2192. # as it should have been already done elsewhere, but for the time being
  2193. # we do it here:
  2194. $mailbox = $1 if $mailbox =~ /^ \s* < ( .* ) > \s* \z/xs;
  2195. my($localpart,$domain) = split_address($mailbox);
  2196. $localpart =~ s/ " | \\ (.) | \\ \z /$1/xsg; # unquote quoted-pairs
  2197. $localpart . $domain;
  2198. }
  2199. # Prepare a single SMTP response and an exit status as per sysexits.h
  2200. # from individual per-recipient response codes, taking into account
  2201. # sendmail milter specifics. Returns a triple: (smtp response, exit status,
  2202. # an indication whether DSN is needed).
  2203. #
  2204. sub one_response_for_all($$$) {
  2205. my($msginfo, $dsn_per_recip_capable, $am_id) = @_;
  2206. my($smtp_resp, $exit_code, $dsn_needed);
  2207. my($delivery_method) = $msginfo->delivery_method;
  2208. my($sender) = $msginfo->sender;
  2209. my($per_recip_data) = $msginfo->per_recip_data;
  2210. my($any_not_done) = scalar(grep { !$_->recip_done } @$per_recip_data);
  2211. if ($delivery_method ne '' && $any_not_done)
  2212. { die "Explicit forwarding, but not all recips done" }
  2213. if (!@$per_recip_data) { # no recipients, nothing to do
  2214. $smtp_resp = "250 2.5.0 Ok, id=$am_id"; $exit_code = EX_OK;
  2215. do_log(5, "one_response_for_all <$sender>: no recipients, '$smtp_resp'");
  2216. }
  2217. if (!defined $smtp_resp) {
  2218. for my $r (@$per_recip_data) { # any 4xx code ?
  2219. if ($r->recip_smtp_response =~ /^4/) # pick the first 4xx code
  2220. { $smtp_resp = $r->recip_smtp_response; last }
  2221. }
  2222. if (!defined $smtp_resp) {
  2223. for my $r (@$per_recip_data) { # any invalid code ?
  2224. if ($r->recip_done && $r->recip_smtp_response !~ /^[245]/) {
  2225. $smtp_resp = '451 4.5.0 Bad SMTP response code??? "'
  2226. . $r->recip_smtp_response . '"';
  2227. last; # pick the first
  2228. }
  2229. }
  2230. }
  2231. if (defined $smtp_resp) {
  2232. $exit_code = EX_TEMPFAIL;
  2233. do_log(5, "one_response_for_all <$sender>: 4xx found, '$smtp_resp'");
  2234. }
  2235. }
  2236. # NOTE: a 2xx SMTP response code is set both by internal Discard
  2237. # and by a genuine successful delivery. To distinguish between the two
  2238. # we need to check $r->recip_destiny as well.
  2239. #
  2240. if (!defined $smtp_resp) {
  2241. # if destiny for _all_ recipients is D_DISCARD, give Discard
  2242. my($notall);
  2243. for my $r (@$per_recip_data) {
  2244. if ($r->recip_destiny == D_DISCARD) # pick the first DISCARD code
  2245. { $smtp_resp = $r->recip_smtp_response if !defined $smtp_resp }
  2246. else { $notall++; last } # one is not a discard, nogood
  2247. }
  2248. if ($notall) { $smtp_resp = undef }
  2249. if (defined $smtp_resp) {
  2250. # helper program will interpret 99 as discard
  2251. $exit_code = $delivery_method eq '' ? 99 : EX_OK;
  2252. do_log(5, "one_response_for_all <$sender>: all DISCARD, '$smtp_resp'");
  2253. }
  2254. }
  2255. if (!defined $smtp_resp) {
  2256. # destiny for _all_ recipients is Discard or Reject, give 5xx
  2257. # (and there is at least one Reject)
  2258. my($notall, $done_level);
  2259. my($bounce_cnt) = 0;
  2260. for my $r (@$per_recip_data) {
  2261. my($dest, $resp) = ($r->recip_destiny, $r->recip_smtp_response);
  2262. if ($dest == D_DISCARD) {
  2263. # ok, this one is discard, let's see the rest
  2264. } elsif ($resp =~ /^5/ && $dest != D_BOUNCE) {
  2265. # prefer to report SMTP response code of genuine rejects
  2266. # from MTA, over internal rejects by content filters
  2267. if (!defined $smtp_resp || $r->recip_done > $done_level)
  2268. { $smtp_resp = $resp; $done_level = $r->recip_done }
  2269. } else { $notall++; last } # one is Pass or Bounce, nogood
  2270. }
  2271. if ($notall) { $smtp_resp = undef }
  2272. if (defined $smtp_resp) {
  2273. $exit_code = EX_UNAVAILABLE;
  2274. do_log(5, "one_response_for_all <$sender>: REJECTs, '$smtp_resp'");
  2275. }
  2276. }
  2277. if (!defined $smtp_resp) {
  2278. # mixed destiny => 2xx, but generate dsn for bounces and rejects
  2279. my($rej_cnt) = 0; my($bounce_cnt) = 0; my($drop_cnt) = 0;
  2280. for my $r (@$per_recip_data) {
  2281. my($dest, $resp) = ($r->recip_destiny, $r->recip_smtp_response);
  2282. if ($resp =~ /^2/ && $dest == D_PASS) # genuine successful delivery
  2283. { $smtp_resp = $resp if !defined $smtp_resp }
  2284. $drop_cnt++ if $dest == D_DISCARD;
  2285. if ($resp =~ /^5/)
  2286. { if ($dest == D_BOUNCE) { $bounce_cnt++ } else { $rej_cnt++ } }
  2287. }
  2288. $exit_code = EX_OK;
  2289. if (!defined $smtp_resp) { # no genuine Pass/2xx
  2290. # declare success, we'll handle bounce
  2291. $smtp_resp = "250 2.5.0 Ok, id=$am_id";
  2292. if ($any_not_done) { $smtp_resp .= ", continue delivery" }
  2293. elsif ($delivery_method eq '') { $exit_code = 99 } # milter DISCARD
  2294. }
  2295. if ($rej_cnt + $bounce_cnt + $drop_cnt > 0) {
  2296. $smtp_resp .= ", ";
  2297. $smtp_resp .= "but " if $rej_cnt+$bounce_cnt+$drop_cnt<@$per_recip_data;
  2298. $smtp_resp .= join ", and ",
  2299. map { my($cnt, $nm) = @$_;
  2300. !$cnt ? () : $cnt == @$per_recip_data ? $nm : "$cnt $nm"
  2301. } ([$rej_cnt,'REJECT'], [$bounce_cnt,'BOUNCE'], [$drop_cnt,'DISCARD']);
  2302. }
  2303. $dsn_needed =
  2304. ($bounce_cnt > 0 || ($rej_cnt > 0 && !$dsn_per_recip_capable)) ? 1 : 0;
  2305. ll(5) && do_log(5,"one_response_for_all <$sender>: "
  2306. . ($rej_cnt + $bounce_cnt + $drop_cnt > 0 ? 'mixed' : 'success')
  2307. . ", r=$rej_cnt,b=$bounce_cnt,d=$drop_cnt"
  2308. . ", dsn_needed=$dsn_needed, '$smtp_resp'");
  2309. }
  2310. ($smtp_resp, $exit_code, $dsn_needed);
  2311. }
  2312. 1;
  2313. #
  2314. package Amavis::Lookup::RE;
  2315. use strict;
  2316. use re 'taint';
  2317. BEGIN {
  2318. use Exporter ();
  2319. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  2320. $VERSION = '2.043';
  2321. @ISA = qw(Exporter);
  2322. }
  2323. BEGIN { import Amavis::Util qw(ll do_log fmt_struct) }
  2324. # Make an object out of the supplied lookup list
  2325. # to make it distinguishable from simple ACL array
  2326. sub new($$) { my($class) = shift; bless [@_], $class }
  2327. # lookup_re() performs a lookup for an e-mail address or other key string
  2328. # against a list made up of regular expressions.
  2329. #
  2330. # A full unmodified e-mail address is always used, so splitting to localpart
  2331. # and domain or lowercasing is NOT performed. The regexp is powerful enough
  2332. # that this can be accomplished by its mechanisms. The routine is useful for
  2333. # other RE tests besides the usual e-mail addresses, such as looking for
  2334. # banned file names.
  2335. #
  2336. # Each element of the list can be ref to a pair, or directly a regexp
  2337. # ('Regexp' object created by a qr operator, or just a (less efficient)
  2338. # string containing a regular expression). If it is a pair, the first
  2339. # element is treated as a regexp, and the second provides a value in case
  2340. # the regexp matches. If not a pair, the implied result of a match is 1.
  2341. #
  2342. # The regular expression is taken as-is, no implicit anchoring or setting
  2343. # case insensitivity is done, so do use a qr'(?i)^user@example\.com$',
  2344. # and not a sloppy qr'user@example.com', which can easily backfire.
  2345. # Also, if qr is used with a delimiter other than ' (apostrophe), make sure
  2346. # to quote the @ and $ .
  2347. #
  2348. # The pattern allows for capturing of parenthesized substrings, which can
  2349. # then be referenced from the result string using the $1, $2, ... notation,
  2350. # as with the Perl m// operator. The number after a $ may be a multi-digit
  2351. # decimal number. To avoid possible ambiguity the ${n} or $(n) form may be used
  2352. # Substring numbering starts with 1. Nonexistent references evaluate to empty
  2353. # strings. If any substitution is done, the result inherits the taintedness
  2354. # of $addr. Keep in mind that $ and @ characters needs to be backslash-quoted
  2355. # in qq() strings. Example:
  2356. # $virus_quarantine_to = new_RE(
  2357. # [ qr'^(.*)@example\.com$'i => 'virus-${1}@example.com' ],
  2358. # [ qr'^(.*)(@[^@]*)?$'i => 'virus-${1}${2}' ] );
  2359. #
  2360. # Example (equivalent to the example in lookup_acl):
  2361. # $acl_re = Amavis::Lookup::RE->new(
  2362. # qr'@me\.ac\.uk$'i, [qr'[@.]ac\.uk$'i=>0], qr'\.uk$'i );
  2363. # ($r,$k) = $acl_re->lookup_re('user@me.ac.uk');
  2364. # or $r = lookup(0, 'user@me.ac.uk', $acl_re);
  2365. #
  2366. # 'user@me.ac.uk' matches me.ac.uk, returns true and search stops
  2367. # 'user@you.ac.uk' matches .ac.uk, returns false (because of =>0) and search stops
  2368. # 'user@them.co.uk' matches .uk, returns true and search stops
  2369. # 'user@some.com' does not match anything, falls through and returns false (undef)
  2370. #
  2371. # As a special allowance, the $addr argument may be a ref to a list of search
  2372. # keys. At each step in traversing the supplied regexp list, all elements of
  2373. # @$addr are tried. If any of them matches, the search stops. This is currently
  2374. # used in banned names lookups, where all attributes of a part are given as a
  2375. # list @$addr.
  2376. sub lookup_re($$;$) {
  2377. my($self, $addr,$get_all) = @_;
  2378. local($1,$2,$3,$4); my(@matchingkey,@result);
  2379. for my $e (@$self) { # try each regexp in the list
  2380. my($key,$r);
  2381. if (ref($e) eq 'ARRAY') { # a pair: (regexp,result)
  2382. ($key,$r) = ($e->[0], @$e < 2 ? 1 : $e->[1]);
  2383. } else { # a single regexp (not a pair), implies result 1
  2384. ($key,$r) = ($e, 1);
  2385. }
  2386. ""=~/x{0}/; # braindead Perl: serves as explicit deflt for an empty regexp
  2387. my(@rhs); # match, capturing parenthesized subpatterns in @rhs
  2388. if (!ref($addr)) { @rhs = $addr =~ /$key/ }
  2389. else { for (@$addr) { @rhs = /$key/; last if @rhs } }
  2390. if (@rhs) { # regexp matches
  2391. # do the righthand side replacements if any $n, ${n} or $(n) is specified
  2392. if (!ref($r) && $r=~/\$/) {
  2393. my($any) = $r =~ s{ \$ ( (\d+) | \{ (\d+) \} | \( (\d+) \) ) }
  2394. { my($j)=$2+$3+$4; $j<1 ? '' : $rhs[$j-1] }gxse;
  2395. # bring taintedness of input to the result
  2396. $r .= substr($addr,0,0) if $any;
  2397. }
  2398. push(@result,$r); push(@matchingkey,$key);
  2399. last if !$get_all;
  2400. }
  2401. }
  2402. if (!ll(5)) {
  2403. # don't bother preparing log report which will not be printed
  2404. } elsif (!@result) {
  2405. do_log(5,sprintf("lookup_re(%s), no matches", fmt_struct($addr)));
  2406. } else { # pretty logging
  2407. my(%esc) = (r => "\r", n => "\n", f => "\f", b => "\b",
  2408. e => "\e", a => "\a", t => "\t");
  2409. my(@mk) = @matchingkey;
  2410. for my $mk (@mk) # undo the \-quoting, will be redone by logging routines
  2411. { $mk =~ s{ \\(.) }{ exists($esc{$1}) ? $esc{$1} : $1 }egsx }
  2412. if (!$get_all) { # first match wins
  2413. do_log(5,sprintf('lookup_re(%s) matches key "%s", result=%s',
  2414. fmt_struct($addr), $mk[0], fmt_struct($result[0])));
  2415. } else { # want all matches
  2416. do_log(5,sprintf("lookup_re(%s) matches keys: %s", fmt_struct($addr),
  2417. join(', ', map {sprintf('"%s"=>%s', $mk[$_],fmt_struct($result[$_]))}
  2418. (0..$#result))));
  2419. }
  2420. }
  2421. if (!$get_all) { !wantarray ? $result[0] : ($result[0], $matchingkey[0]) }
  2422. else { !wantarray ? \@result : (\@result, \@matchingkey) }
  2423. }
  2424. 1;
  2425. #
  2426. package Amavis::Lookup::IP;
  2427. use strict;
  2428. use re 'taint';
  2429. BEGIN {
  2430. use Exporter ();
  2431. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  2432. $VERSION = '2.043';
  2433. @ISA = qw(Exporter);
  2434. @EXPORT_OK = qw(&lookup_ip_acl);
  2435. }
  2436. use subs @EXPORT_OK;
  2437. BEGIN {
  2438. import Amavis::Util qw(ll do_log);
  2439. }
  2440. # ip_to_vec() takes IPv6 or IPv4 IP address with optional prefix length
  2441. # (or IPv4 mask), parses and validates it, and returns it as a 128-bit
  2442. # vector string that can be used as operand to Perl bitwise string operators.
  2443. # Syntax and other errors in the argument throw exception (die).
  2444. # If the second argument $allow_mask is 0, the prefix length or mask
  2445. # specification is not allowed as part of the IP address.
  2446. #
  2447. # The IPv6 syntax parsing and validation adheres to rfc3513.
  2448. # All the following IPv6 address forms are supported:
  2449. # x:x:x:x:x:x:x:x preferred form
  2450. # x:x:x:x:x:x:d.d.d.d alternative form
  2451. # ...::... zero-compressed form
  2452. # addr/prefix-length prefix length may be specified (defaults to 128)
  2453. # Optionally an "IPv6:" prefix may be prepended to the IPv6 address
  2454. # as specified by rfc2821. Brackets enclosing the address are allowed
  2455. # for Postfix compatibility, e.g. [::1]/128 .
  2456. #
  2457. # The following IPv4 forms are allowed:
  2458. # d.d.d.d
  2459. # d.d.d.d/prefix-length CIDR mask length is allowed (defaults to 32)
  2460. # d.d.d.d/m.m.m.m network mask (gets converted to prefix-length)
  2461. # If prefix-length or a mask is specified with an IPv4 address, the address
  2462. # may be shortened to d.d.d/n or d.d/n or d/n. Such truncation is allowed
  2463. # for compatibility with earlier version, but is deprecated and is not
  2464. # allowed for IPv6 addresses.
  2465. #
  2466. # IPv4 addresses and masks are converted to IPv4-mapped IPv6 addresses
  2467. # of the form ::FFFF:d.d.d.d, The CIDR mask length (0..32) is converted
  2468. # to IPv6 prefix-length (96..128). The returned vector strings resulting
  2469. # from IPv4 and IPv6 forms are indistinguishable.
  2470. #
  2471. # NOTE:
  2472. # d.d.d.d is equivalent to ::FFFF:d.d.d.d (IPv4-mapped IPv6 address)
  2473. # which is not the same as ::d.d.d.d (IPv4-compatible IPv6 address)
  2474. #
  2475. # A triple is returned:
  2476. # - IP address represented as a 128-bit vector (a string)
  2477. # - network mask derived from prefix length, a 128-bit vector (string)
  2478. # - prefix length as an integer (0..128)
  2479. #
  2480. sub ip_to_vec($;$) {
  2481. my($ip,$allow_mask) = @_;
  2482. my($ip_len); my(@ip_fields);
  2483. local($1,$2,$3,$4,$5,$6);
  2484. $ip =~ s/^[ \t]+//; $ip =~ s/[ \t\n]+\z//s; # trim
  2485. my($ipa) = $ip;
  2486. ($ipa,$ip_len) = ($1,$2) if $allow_mask && $ip =~ m{^([^/]*)/(.*)\z}s;
  2487. $ipa = $1 if $ipa =~ m{^ \[ (.*) \] \z}xs; # discard optional brackets
  2488. $ipa = $1 if $ipa =~ m{^(.*)%[A-Za-z0-9]+\z}s; # discard interface spec
  2489. if ($ipa =~ m{^(IPv6:)?(.*:)(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\z}si){
  2490. # IPv6 alternative form x:x:x:x:x:x:d.d.d.d
  2491. my(@d) = ($3,$4,$5,$6);
  2492. !grep {$_ > 255} @d
  2493. or die "Invalid decimal field value in IPv6 address: [$ip]\n";
  2494. $ipa = $2 . sprintf("%02X%02X:%02X%02X", @d);
  2495. } elsif ($ipa =~ m{^\d{1,3}(?:\.\d{1,3}){0,3}\z}) { # IPv4 form
  2496. my(@d) = split(/\./,$ipa,-1);
  2497. !grep {$_ > 255} @d
  2498. or die "Invalid field value in IPv4 address: [$ip]\n";
  2499. defined($ip_len) || @d==4
  2500. or die "IPv4 address [$ip] contains fewer than 4 fields\n";
  2501. $ipa = '::FFFF:' . sprintf("%02X%02X:%02X%02X", @d); # IPv4-mapped IPv6
  2502. if (!defined($ip_len)) { $ip_len = 32; # no length, defaults to /32
  2503. } elsif ($ip_len =~ /^\d{1,9}\z/) { # /n, IPv4 CIDR notation
  2504. } elsif ($ip_len =~ /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\z/) {
  2505. !grep {$_ > 255} ($1,$2,$3,$4)
  2506. or die "Illegal field value in IPv4 mask: [$ip]\n";
  2507. my($mask1) = pack('C4',$1,$2,$3,$4); # /m.m.m.m
  2508. my($len) = unpack("%b*",$mask1); # count ones
  2509. my($mask2) = pack('B32', '1' x $len); # reconstruct mask from count
  2510. $mask1 eq $mask2
  2511. or die "IPv4 mask not representing valid CIDR mask: [$ip]\n";
  2512. $ip_len = $len;
  2513. } else {
  2514. die "Invalid IPv4 network mask or CIDR prefix length: [$ip]\n";
  2515. }
  2516. $ip_len<=32 or die "IPv4 network prefix length greater than 32: [$ip]\n";
  2517. $ip_len += 128-32; # convert IPv4 net mask length to IPv6 prefix length
  2518. }
  2519. $ip_len = 128 if !defined($ip_len);
  2520. $ip_len<=128 or die "IPv6 network prefix length greater than 128: [$ip]\n";
  2521. $ipa =~ s/^IPv6://i;
  2522. # now we presumably have an IPv6 preferred form x:x:x:x:x:x:x:x
  2523. if ($ipa !~ /^(.*?)::(.*)\z/s) { # zero-compressing form used?
  2524. @ip_fields = split(/:/,$ipa,-1); # no
  2525. } else { # expand zero-compressing form
  2526. my(@a) = split(/:/,$1,-1); my(@b) = split(/:/,$2,-1);
  2527. my($missing_cnt) = 8-(@a+@b); $missing_cnt = 1 if $missing_cnt<1;
  2528. @ip_fields = (@a, (0) x $missing_cnt, @b);
  2529. }
  2530. !grep { !/^[0-9a-zA-Z]{1,4}\z/ } @ip_fields # this is quite slow
  2531. or die "Invalid syntax of IPv6 address: [$ip]\n";
  2532. @ip_fields<8 and die "IPv6 address [$ip] contains fewer than 8 fields\n";
  2533. @ip_fields>8 and die "IPv6 address [$ip] contains more than 8 fields\n";
  2534. my($vec) = pack("n8", map {hex} @ip_fields);
  2535. $ip_len=~/^\d{1,3}\z/
  2536. or die "Invalid prefix length syntax in IP address: [$ip]\n";
  2537. $ip_len<=128 or die "Invalid prefix length in IPv6 address: [$ip]\n";
  2538. my($mask) = pack('B128', '1' x $ip_len);
  2539. # do_log(5,sprintf("ip_to_vec: %s => %s/%d\n", $ip,unpack("B*",$vec),$ip_len));
  2540. ($vec,$mask,$ip_len);
  2541. }
  2542. # lookup_ip_acl() performs a lookup for an IPv4 or IPv6 address
  2543. # against access control list or a hash of network or host addresses.
  2544. #
  2545. # IP address is compared to each member of an access list in turn,
  2546. # the first match wins (terminates the search), and its value decides
  2547. # whether the result is true (yes, permit, pass) or false (no, deny, drop).
  2548. # Falling through without a match produces false (undef).
  2549. #
  2550. # The presence of character '!' prepended to a list member decides
  2551. # whether the result will be true (without a '!') or false (with '!')
  2552. # in case this list member matches and terminates the search.
  2553. #
  2554. # Because search stops at the first match, it only makes sense
  2555. # to place more specific patterns before the more general ones.
  2556. #
  2557. # For IPv4 a network address can be specified in classless notation
  2558. # n.n.n.n/k, or using a mask n.n.n.n/m.m.m.m . Missing mask implies /32,
  2559. # i.e. a host address. For IPv6 addresses all rfc3513 forms are allowed.
  2560. # See also comments at ip_to_vec().
  2561. #
  2562. # Although not a special case, it is good to remember that '::/0'
  2563. # always matches any IPv4 or IPv6 address (even syntactically invalid address).
  2564. #
  2565. # The '0/0' is equivalent to '::FFFF:0:0/96' and matches any syntactically
  2566. # valid IPv4 address (including IPv4-mapped IPv6 addresses), but not other
  2567. # IPv6 addresses!
  2568. #
  2569. # Example
  2570. # given: @acl = qw( !192.168.1.12 172.16.3.3 !172.16.3.0/255.255.255.0
  2571. # 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16
  2572. # !0.0.0.0/8 !:: 127.0.0.0/8 ::1 );
  2573. # matches rfc1918 private address space except host 192.168.1.12
  2574. # and net 172.16.3/24 (but host 172.16.3.3 within 172.16.3/24 still matches).
  2575. # In addition, the 'unspecified' (null, i.e. all zeros) IPv4 and IPv6
  2576. # addresses return false, and IPv4 and IPv6 loopback addresses match
  2577. # and return true.
  2578. #
  2579. # If the supplied lookup table is a hash reference, match a canonical IP
  2580. # address: dot-quad IPv4, or preferred IPv6 form, against hash keys. For IPv4
  2581. # addresses a simple classful subnet specification is allowed in hash keys
  2582. # by truncating trailing bytes from the looked up IPv4 address. A syntactically
  2583. # invalid IP address can only match a hash entry with an undef key.
  2584. #
  2585. sub lookup_ip_acl($@) {
  2586. my($ip, @nets_ref) = @_;
  2587. my($ip_vec,$ip_mask) = eval { ip_to_vec($ip,0) }; my($eval_stat) = $@;
  2588. my($label,$fullkey,$result); my($found) = 0;
  2589. for my $tb (@nets_ref) {
  2590. my($t) = ref($tb) eq 'REF' ? $$tb : $tb; # allow one level of indirection
  2591. if (!ref($t) || ref($t) eq 'SCALAR') { # a scalar always matches
  2592. my($r) = ref($t) ? $$t : $t; # allow direct or indirect reference
  2593. $result = $r; $fullkey = "(constant:$r)";
  2594. $found++ if defined $result;
  2595. } elsif (ref($t) eq 'HASH') {
  2596. if (!defined $ip_vec) { # syntactically invalid IP address
  2597. $fullkey = undef; $result = $t->{$fullkey};
  2598. $found++ if defined $result;
  2599. } else { # valid IP address
  2600. # match the canonical IP address: dot-quad IPv4, or preferred IPv6 form
  2601. my($ip_c); # IP address in the canonical form: x:x:x:x:x:x:x:x
  2602. my($ip_dq); # IPv4 in a dotted-quad form if IPv4-mapped, or undef
  2603. $ip_c = join(':', map {sprintf('%04x',$_)} unpack('n8',$ip_vec));
  2604. my($ipv4_vec,$ipv4_mask) = ip_to_vec('::FFFF:0:0/96',1);
  2605. if ( ($ip_vec & $ipv4_mask) eq ($ipv4_vec & $ipv4_mask) ) {
  2606. # is an IPv4-mapped IPv6 address, format it in a dot-quad form
  2607. $ip_dq = join('.', unpack('C4',substr($ip_vec,12,4))); # last 32 bits
  2608. }
  2609. do_log(5, "lookup_ip_acl keys: \"$ip_dq\", \"$ip_c\"");
  2610. if (defined $ip_dq) { # try dot-quad if applicable
  2611. for (my(@f)=split(/\./,$ip_dq); @f && !$found; $#f--) {
  2612. $fullkey = join('.',@f); $result = $t->{$fullkey};
  2613. $found++ if defined $result;
  2614. }
  2615. }
  2616. if (!$found) { # try the 'preferred IPv6 form'
  2617. $fullkey = $ip_c; $result = $t->{$fullkey};
  2618. $found++ if defined $result;
  2619. }
  2620. }
  2621. } elsif (ref($t) eq 'ARRAY') {
  2622. my($key, $acl_ip_vec, $acl_mask, $acl_mask_len);
  2623. for my $net (@$t) {
  2624. $fullkey = $key = $net; $result = 1;
  2625. if ($key =~ /^(!+)(.*)\z/s) { # starts with exclamation mark(s)
  2626. $key = $2;
  2627. $result = 1 - $result if (length($1) & 1); # negate if odd
  2628. }
  2629. ($acl_ip_vec, $acl_mask, $acl_mask_len) = ip_to_vec($key,1);
  2630. if ($acl_mask_len == 0) { $found++ } # even invalid address matches /0
  2631. elsif (!defined($ip_vec)) {} # no other matches for invalid address
  2632. elsif (($ip_vec & $acl_mask) eq ($acl_ip_vec & $acl_mask)) { $found++ }
  2633. last if $found;
  2634. }
  2635. } elsif ($t->isa('Amavis::Lookup::IP')) { # pre-parsed IP lookup array obj
  2636. my($acl_ip_vec, $acl_mask, $acl_mask_len);
  2637. for my $e (@$t) {
  2638. ($fullkey, $acl_ip_vec, $acl_mask, $acl_mask_len, $result) = @$e;
  2639. if ($acl_mask_len == 0) { $found++ } # even invalid address matches /0
  2640. elsif (!defined($ip_vec)) {} # no other matches for invalid address
  2641. elsif (($ip_vec & $acl_mask) eq ($acl_ip_vec & $acl_mask)) { $found++ }
  2642. last if $found;
  2643. }
  2644. } elsif ($t->isa('Amavis::Lookup::Label')) { # logging label
  2645. # just a convenience for logging purposes, not a real lookup method
  2646. $label = $t->display; # grab the name, and proceed with the next table
  2647. } else {
  2648. die "TROUBLE: lookup table is an unknown object: " . ref($t);
  2649. }
  2650. last if $found;
  2651. }
  2652. $fullkey = $result = undef if !$found;
  2653. if ($label ne '') { $label = " ($label)" }
  2654. ll(4) && do_log(4, "lookup_ip_acl$label: key=\"$ip\""
  2655. . (!$found ? ", no match" : " matches \"$fullkey\", result=$result"));
  2656. if ($eval_stat eq '') { $eval_stat = undef }
  2657. else {
  2658. chomp($eval_stat); $eval_stat = "lookup_ip_acl$label: $eval_stat";
  2659. do_log(2, $eval_stat);
  2660. }
  2661. !wantarray ? $result : ($result, $fullkey, $eval_stat);
  2662. }
  2663. # create a pre-parsed object from a list of IP networks,
  2664. # which may be used as an argument to lookup_ip_acl to speed up its searches
  2665. sub new($@) {
  2666. my($class,@nets) = @_;
  2667. my(@list);
  2668. for my $net (@nets) {
  2669. my($key) = $net; my($result) = 1;
  2670. if ($key =~ /^(!+)(.*)\z/s) { # starts with exclamation mark(s)
  2671. $key = $2;
  2672. $result = 1 - $result if (length($1) & 1); # negate if odd
  2673. }
  2674. my($ip_vec, $ip_mask, $ip_mask_len) = ip_to_vec($key,1);
  2675. push(@list, [$net, $ip_vec, $ip_mask, $ip_mask_len, $result]);
  2676. }
  2677. bless \@list, $class;
  2678. }
  2679. 1;
  2680. #
  2681. package Amavis::Lookup::Label;
  2682. use strict;
  2683. use re 'taint';
  2684. # Make an object out of the supplied string, to serve as label
  2685. # in log messages generated by sub lookup
  2686. sub new($$) { my($class) = shift; my($str) = shift; bless \$str, $class }
  2687. sub display($) { my($self) = shift; $$self }
  2688. 1;
  2689. #
  2690. package Amavis::Lookup;
  2691. use strict;
  2692. use re 'taint';
  2693. BEGIN {
  2694. use Exporter ();
  2695. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  2696. $VERSION = '2.043';
  2697. @ISA = qw(Exporter);
  2698. @EXPORT_OK = qw(&lookup);
  2699. }
  2700. use subs @EXPORT_OK;
  2701. BEGIN {
  2702. import Amavis::Util qw(ll do_log fmt_struct);
  2703. import Amavis::Conf qw(:platform c cr ca);
  2704. import Amavis::Timing qw(section_time);
  2705. import Amavis::rfc2821_2822_Tools qw(split_address make_query_keys);
  2706. }
  2707. # lookup_hash() performs a lookup for an e-mail address against a hash map.
  2708. # If a match is found (a hash key exists in the Perl hash) the function returns
  2709. # whatever the map returns, otherwise undef is returned. First match wins,
  2710. # aborting further search sequence.
  2711. #
  2712. sub lookup_hash($$;$) {
  2713. my($addr, $hash_ref,$get_all) = @_;
  2714. (ref($hash_ref) eq 'HASH')
  2715. or die "lookup_hash: arg2 must be a hash ref: $hash_ref";
  2716. local($1,$2,$3,$4); my(@matchingkey,@result);
  2717. my($keys_ref,$rhs_ref) = make_query_keys($addr,1,1);
  2718. for my $key (@$keys_ref) { # do the search
  2719. if (exists $$hash_ref{$key}) { # got it
  2720. push(@result,$$hash_ref{$key}); push(@matchingkey,$key);
  2721. last if !$get_all;
  2722. }
  2723. }
  2724. # do the right-hand side replacements if any $n, ${n} or $(n) is specified
  2725. for my $r (@result) { # remember that $r is just an alias to array elements
  2726. if (!ref($r) && $r=~/\$/) { # is a plain string containing a '$'
  2727. my($any) = $r =~ s{ \$ ( (\d+) | \{ (\d+) \} | \( (\d+) \) ) }
  2728. { my($j)=$2+$3+$4; $j<1 ? '' : $rhs_ref->[$j-1] }gxse;
  2729. # bring taintedness of input to the result
  2730. $r .= substr($addr,0,0) if $any;
  2731. }
  2732. }
  2733. if (!ll(5)) {
  2734. # only bother with logging when needed
  2735. } elsif (!@result) {
  2736. do_log(5,"lookup_hash($addr), no matches");
  2737. } elsif (!$get_all) { # first match wins
  2738. do_log(5,sprintf('lookup_hash(%s) matches key "%s", result=%s',
  2739. $addr,$matchingkey[0],$result[0]));
  2740. } else { # want all matches
  2741. do_log(5,"lookup_hash($addr) matches keys: ".
  2742. join(', ', map {sprintf('"%s"=>%s',$matchingkey[$_],$result[$_])}
  2743. (0..$#result)));
  2744. }
  2745. if (!$get_all) { !wantarray ? $result[0] : ($result[0], $matchingkey[0]) }
  2746. else { !wantarray ? \@result : (\@result, \@matchingkey) }
  2747. }
  2748. # lookup_acl() performs a lookup for an e-mail address against
  2749. # access control list.
  2750. #
  2751. # The supplied e-mail address is compared with each member of the
  2752. # lookup list in turn, the first match wins (terminates the search),
  2753. # and its value decides whether the result is true (yes, permit, pass)
  2754. # or false (no, deny, drop). Falling through without a match
  2755. # produces false (undef). Search is case-insensitive.
  2756. #
  2757. # lookup_acl is not aware of address extensions and they are not
  2758. # handled specially.
  2759. #
  2760. # If a list element contains a '@', the full e-mail address is compared,
  2761. # otherwise if a list element has a leading dot, the domain name part is
  2762. # matched only, and the domain as well as its subdomains can match. If there
  2763. # is no leading dot, the domain must match exactly (subdomains do not match).
  2764. #
  2765. # The presence of character '!' prepended to a list element decides
  2766. # whether the result will be true (without a '!') or false (with '!')
  2767. # in case this list element matches and terminates the search.
  2768. #
  2769. # Because search stops at the first match, it only makes sense
  2770. # to place more specific patterns before the more general ones.
  2771. #
  2772. # Although not a special case, it is good to remember that '.' always matches,
  2773. # so a '.' would stop the search and return true, whereas '!.' would stop the
  2774. # search and return false (0).
  2775. #
  2776. # Examples:
  2777. #
  2778. # given: @acl = qw( me.ac.uk !.ac.uk .uk )
  2779. # 'me.ac.uk' matches me.ac.uk, returns true and search stops
  2780. #
  2781. # given: @acl = qw( me.ac.uk !.ac.uk .uk )
  2782. # 'you.ac.uk' matches .ac.uk, returns false (because of '!') and search stops
  2783. #
  2784. # given: @acl = qw( me.ac.uk !.ac.uk .uk )
  2785. # 'them.co.uk' matches .uk, returns true and search stops
  2786. #
  2787. # given: @acl = qw( me.ac.uk !.ac.uk .uk )
  2788. # 'some.com' does not match anything, falls through and returns false (undef)
  2789. #
  2790. # given: @acl = qw( me.ac.uk !.ac.uk .uk !. )
  2791. # 'some.com' similar to previous, except it returns 0 instead of undef,
  2792. # which would only make a difference if this ACL is not the last argument
  2793. # in a call to lookup()
  2794. #
  2795. # given: @acl = qw( me.ac.uk !.ac.uk .uk . )
  2796. # 'some.com' matches catchall ".", and returns true. The ".uk" is redundant
  2797. #
  2798. # more complex example: @acl = qw(
  2799. # !The.Boss@dept1.xxx.com .dept1.xxx.com
  2800. # .dept2.xxx.com .dept3.xxx.com lab.dept4.xxx.com
  2801. # sub.xxx.com !.sub.xxx.com
  2802. # me.d.aaa.com him.d.aaa.com !.d.aaa.com .aaa.com
  2803. # );
  2804. sub lookup_acl($$) {
  2805. my($addr, $acl_ref) = @_;
  2806. (ref($acl_ref) eq 'ARRAY')
  2807. or die "lookup_acl: arg2 must be a list ref: $acl_ref";
  2808. return undef if !@$acl_ref; # empty list can't match anything
  2809. my($lpcs) = c('localpart_is_case_sensitive');
  2810. my($localpart,$domain) = split_address($addr); $domain = lc($domain);
  2811. $localpart = lc($localpart) if !$lpcs;
  2812. local($1,$2);
  2813. # chop off leading @ and trailing dots
  2814. $domain = $1 if $domain =~ /^\@?(.*?)\.*\z/s;
  2815. my($lcaddr) = $localpart . '@' . $domain;
  2816. my($matchingkey, $result); my($found) = 0;
  2817. for my $e (@$acl_ref) {
  2818. $result = 1; $matchingkey = $e; my($key) = $e;
  2819. if ($key =~ /^(!+)(.*)\z/s) { # starts with an exclamation mark(s)
  2820. $key = $2;
  2821. $result = 1-$result if (length($1) & 1); # negate if odd
  2822. }
  2823. if ($key =~ /^(.*?)\@([^@]*)\z/s) { # contains '@', check full address
  2824. $found++ if $localpart eq ($lpcs?$1:lc($1)) && $domain eq lc($2);
  2825. } elsif ($key =~ /^\.(.*)\z/s) { # leading dot: domain or subdomain
  2826. my($key_t) = lc($1);
  2827. $found++ if $domain eq $key_t || $domain =~ /(\.|\z)\Q$key_t\E\z/s;
  2828. } else { # match domain (but not its subdomains)
  2829. $found++ if $domain eq lc($key);
  2830. }
  2831. last if $found;
  2832. }
  2833. $matchingkey = $result = undef if !$found;
  2834. do_log(5, "lookup_acl($addr)".
  2835. (!$found?", no match":" matches key \"$matchingkey\", result=$result"));
  2836. !wantarray ? $result : ($result, $matchingkey);
  2837. }
  2838. # Perform a lookup for an e-mail address against any number of supplied maps:
  2839. # - SQL map,
  2840. # - LDAP map,
  2841. # - hash map (associative array),
  2842. # - (access control) list,
  2843. # - a list of regular expressions (an Amavis::Lookup::RE object),
  2844. # - a (defined) scalar always matches, and returns itself as the 'map' value
  2845. # (useful as a catchall for final 'pass' or 'fail');
  2846. # (see lookup_hash, lookup_acl, lookup_sql and lookup_ldap for details).
  2847. #
  2848. # when $get_all is 0 (the common usage):
  2849. # If a match is found (a defined value), returns whatever the map returns,
  2850. # otherwise returns undef. FIRST match aborts further search sequence.
  2851. # when $get_all is true:
  2852. # Collects a list of results from ALL matching tables, and within each
  2853. # table from ALL matching key. Returns a ref to the a list of results
  2854. # (and a ref to a list of matching keys if returning a pair).
  2855. # The first element of both lists is supposed to be what lookup() would
  2856. # have returned if $get_all were 0. The order of returned elements
  2857. # corresponds to the order of the search.
  2858. #
  2859. sub lookup($$@) {
  2860. my($get_all, $addr, @tables) = @_;
  2861. my($label, @result,@matchingkey);
  2862. for my $tb (@tables) {
  2863. my($t) = ref($tb) eq 'REF' ? $$tb : $tb; # allow one level of indirection
  2864. if (!ref($t) || ref($t) eq 'SCALAR') { # a scalar always matches
  2865. my($r) = ref($t) ? $$t : $t; # allow direct or indirect reference
  2866. if (defined $r) {
  2867. do_log(5,"lookup: (scalar) matches, result=\"$r\"");
  2868. push(@result,$r); push(@matchingkey,"(constant:$r)");
  2869. }
  2870. } elsif (ref($t) eq 'HASH') {
  2871. my($r,$mk) = lookup_hash($addr,$t,$get_all);
  2872. if (!defined $r) {}
  2873. elsif (!$get_all) { push(@result,$r); push(@matchingkey,$mk) }
  2874. elsif (@$r) { push(@result,@$r); push(@matchingkey,@$mk) }
  2875. } elsif (ref($t) eq 'ARRAY') {
  2876. my($r,$mk) = lookup_acl($addr,$t);
  2877. if (defined $r) { push(@result,$r); push(@matchingkey,$mk) }
  2878. } elsif ($t->isa('Amavis::Lookup::Label')) { # logging label
  2879. # just a convenience for logging purposes, not a real lookup method
  2880. $label = $t->display; # grab the name, and proceed with the next table
  2881. } elsif ($t->isa('Amavis::Lookup::RE')) {
  2882. my($r,$mk) = $t->lookup_re($addr,$get_all);
  2883. if (!defined $r) {}
  2884. elsif (!$get_all) { push(@result,$r); push(@matchingkey,$mk) }
  2885. elsif (@$r) { push(@result,@$r); push(@matchingkey,@$mk) }
  2886. } elsif ($t->isa('Amavis::Lookup::SQL')) {
  2887. my($r,$mk) = $t->lookup_sql($addr,$get_all);
  2888. if (!defined $r) {}
  2889. elsif (!$get_all) { push(@result,$r); push(@matchingkey,$mk) }
  2890. elsif (@$r) { push(@result,@$r); push(@matchingkey,@$mk) }
  2891. } elsif ($t->isa('Amavis::Lookup::SQLfield')) {
  2892. my($r,$mk) = $t->lookup_sql_field($addr,$get_all);
  2893. if (!defined $r) {}
  2894. elsif (!$get_all) { push(@result,$r); push(@matchingkey,$mk) }
  2895. elsif (@$r) { push(@result,@$r); push(@matchingkey,@$mk) }
  2896. } elsif ($t->isa('Amavis::Lookup::LDAP')) {
  2897. my($r,$mk) = $t->lookup_ldap($addr,$get_all);
  2898. if (!defined $r) {}
  2899. elsif (!$get_all) { push(@result,$r); push(@matchingkey,$mk) }
  2900. elsif (@$r) { push(@result,@$r); push(@matchingkey,@$mk) }
  2901. } elsif ($t->isa('Amavis::Lookup::LDAPattr')) {
  2902. my($r,$mk) = $t->lookup_ldap_attr($addr,$get_all);
  2903. if (!defined $r) {}
  2904. elsif (!$get_all) { push(@result,$r); push(@matchingkey,$mk) }
  2905. elsif (@$r) { push(@result,@$r); push(@matchingkey,@$mk) }
  2906. } else {
  2907. die "TROUBLE: lookup table is an unknown object: " . ref($t);
  2908. }
  2909. last if @result && !$get_all;
  2910. }
  2911. # pretty logging
  2912. if (ll(4)) { # only bother preparing log report which will be printed
  2913. if (defined $label && $label ne '') { $label = " ($label)" }
  2914. if (!@tables) {
  2915. do_log(4,sprintf("lookup%s => undef, %s, no lookup tables",
  2916. $label, fmt_struct($addr)));
  2917. } elsif (!@result) {
  2918. do_log(4,sprintf("lookup%s => undef, %s does not match",
  2919. $label, fmt_struct($addr)));
  2920. } elsif (!$get_all) { # first match wins
  2921. do_log(4,sprintf(
  2922. 'lookup%s => %-6s %s matches, result=%s, matching_key="%s"',
  2923. $label, $result[0] ? 'true,' : 'false,',
  2924. fmt_struct($addr), fmt_struct($result[0]), $matchingkey[0]));
  2925. } else { # want all matches
  2926. do_log(4,sprintf('lookup%s, %d matches for %s, results: %s',
  2927. $label, scalar(@result), fmt_struct($addr),
  2928. join(', ',map { sprintf('"%s"=>%s',
  2929. $matchingkey[$_], fmt_struct($result[$_]))
  2930. } (0..$#result) )));
  2931. }
  2932. }
  2933. if (!$get_all) { !wantarray ? $result[0] : ($result[0], $matchingkey[0]) }
  2934. else { !wantarray ? \@result : (\@result, \@matchingkey) }
  2935. }
  2936. 1;
  2937. #
  2938. package Amavis::Expand;
  2939. use strict;
  2940. use re 'taint';
  2941. BEGIN {
  2942. use Exporter ();
  2943. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  2944. $VERSION = '2.043';
  2945. @ISA = qw(Exporter);
  2946. @EXPORT_OK = qw(&expand);
  2947. }
  2948. use subs @EXPORT_OK;
  2949. BEGIN {
  2950. import Amavis::Util qw(ll do_log);
  2951. }
  2952. # Given a string reference and a hashref of predefined (builtin) macros,
  2953. # expand() performs a macro expansion and returns a ref to the resulting string
  2954. #
  2955. # This is a simple, yet fully fledged macro processor with proper lexical
  2956. # analysis, call stack, implied quoting levels, user supplied builtin macros,
  2957. # two builtin flow-control macros: selector and iterator, plus a macro #,
  2958. # which discards input tokens until NEWLINE (like 'dnl' in m4).
  2959. # Also recognized are the usual \c and \nnn forms for specifying special
  2960. # characters, where c can be any of: r, n, f, b, e, a, t. Lexical analysis
  2961. # of the input string is performed only once, macro result values are not
  2962. # in danger of being lexically re-parsed and are treated as plain characters,
  2963. # loosing any special meaning they might have. No new macros can be defined
  2964. # by processing input string (at least in this version).
  2965. #
  2966. # Simple caller-provided macros have a single character name (usually a letter)
  2967. # and can evaluate to a string (possibly empty or undef), or an array of
  2968. # strings. It can also be a subroutine reference, in which case the subroutine
  2969. # will be called whenever macro value is needed. The subroutine must return
  2970. # a scalar: a string, or an array reference. The result will be treated as if
  2971. # it were specified directly.
  2972. #
  2973. # Two forms of simple macro calls are known: %x and %#x (where x is a single
  2974. # letter macro name, i.e. a key in a user-supplied associative array):
  2975. # %x evaluates to the hash value associated with the name x;
  2976. # if the value is an array ref, the result is a single concatenated
  2977. # string of values separated with comma-space pairs;
  2978. # %#x evaluates to a number: if a macro value is a scalar, returns 0
  2979. # for all-whitespace value, and 1 otherwise. If a value is an array ref,
  2980. # evaluates to the number of elements in the array.
  2981. # A macro is evaluated only in nonquoted context, i.e. top-level text or in
  2982. # the first argument of a top-level selector (see below). A literal percent
  2983. # character can be produced by %% or \%.
  2984. #
  2985. # More powerful expansion is provided by two builtin macros, using syntax:
  2986. # [? arg1 | arg2 | ... ] a selector
  2987. # [ arg1 | arg2 | ... ] an iterator
  2988. # where [, [?, | and ] are required tokens. To take away the special meaning
  2989. # of these characters they can be quoted by a backslash, e.g. \[ or \\ .
  2990. # Arguments are arbitrary text, possibly multiline, whitespace counts.
  2991. # Nested macro calls are permitted, proper bracket nesting must be observed.
  2992. #
  2993. # SELECTOR lets its first argument be evaluated immediately, and implicitly
  2994. # protects the remaining arguments. The evaluated first argument chooses which
  2995. # of the remaining arguments is selected as a result value. The chosen result
  2996. # is only then evaluated, remaining arguments are discarded without evaluation.
  2997. # The first argument is usually a number (with optional leading and trailing
  2998. # whitespace). If it is a non-numeric string, it is treated as 0 for
  2999. # all-whitespace, and as 1 otherwise. Value 0 selects the very next (second)
  3000. # argument, value 1 selects the one after it, etc. If the value is greater than
  3001. # the number of available arguments, the last one (unless it is the only one)
  3002. # is selected. If there is only one (the first) alternative available but the
  3003. # value is greater than 0, an empty string is returned.
  3004. # Examples:
  3005. # [? 2 | zero | one | two | three ] -> two
  3006. # [? foo | none | any | two | three ] -> any
  3007. # [? 24 | 0 | one | many ] -> many
  3008. # [? 2 |No recipients] -> (empty string)
  3009. # [? %#R |No recipients|One recipient|%#R recipients]
  3010. # [? %q |No quarantine|Quarantined as %q]
  3011. # Note that a selector macro call can be considered a form of if-then-else,
  3012. # except that the 'then' and 'else' parts are swapped!
  3013. #
  3014. # ITERATOR in its full form takes three arguments (and ignores any extra
  3015. # arguments after that):
  3016. # [ %x | body-usually-containing-%x | separator ]
  3017. # All iterator's arguments are implicitly quoted, iterator performs its own
  3018. # substitutions on provided arguments, as described below. The result of an
  3019. # iterator call is a body (the second argument) repeated as many times as
  3020. # there are elements in the array denoted by the first argument. In each
  3021. # instance of a body all occurrences of a token %x in the body are replaced
  3022. # with each consecutive element of the array. Resulting body instances are
  3023. # then glued together with a string given as the third argument. The result
  3024. # is finally evaluated as any top-level text for possible further expansion.
  3025. #
  3026. # There are two simplified forms of iterator call:
  3027. # [ body | separator ]
  3028. # or [ body ]
  3029. # where missing separator is considered a null string, and a missing formal
  3030. # argument name is obtained by looking for the first token of the form %x
  3031. # in the body. If there is no formal argument specified (neither explicitly
  3032. # nor in the body), the result is an empty string, which is potentially useful
  3033. # as a null lexical separator.
  3034. #
  3035. # Examples:
  3036. # [%V| ] a space-separated list of virus names
  3037. #
  3038. # [%V|\n] a newline-separated list of virus names
  3039. #
  3040. # [%V|
  3041. # ] same thing: a newline-separated list of virus names
  3042. #
  3043. # [
  3044. # %V] a list of virus names, each preceeded by NL and spaces
  3045. #
  3046. # [ %R |%s --> <%R>|, ] a comma-space separated list of sender/recipient
  3047. # name pairs where recipient is iterated over the list
  3048. # of recipients. (Only the (first) token %x in the first
  3049. # argument is significant, other characters are ignored.)
  3050. #
  3051. # [%V|[%R|%R + %V|, ]|; ] produce all combinations of %R + %V elements
  3052. #
  3053. # A combined example:
  3054. # [? %#C |#|Cc: [<%C>|, ]]
  3055. # [? %#C ||Cc: [<%C>|, ]\n]# ... same thing
  3056. # evaluates to an empty string if there are no elements in the %C array,
  3057. # otherwise it evaluates to a line: Cc: <addr1>, <addr2>, ...\n
  3058. # The '#' removes input characters until and including newline after it.
  3059. # It can be used for clarity to allow newlines be placed in the source text
  3060. # but not resulting in empty lines in the expanded text. In the second example
  3061. # above, a backslash at the end of the line would achieve the same result,
  3062. # although the method is different: \NEWLINE is removed during initial lexical
  3063. # analysis, while # is an internal macro which, when called, actively discards
  3064. # tokens following it, until NEWLINE (or end of input) is encountered.
  3065. # Whitespace (including newlines) around the first argument %#C of selector
  3066. # call is ignored and can be used for clarity.
  3067. #
  3068. # These all produce the same result:
  3069. # To: [%T|%T|, ]
  3070. # To: [%T|, ]
  3071. # To: %T
  3072. #
  3073. # See further practical examples in the supplied notification messages;
  3074. # see also README.customize file.
  3075. #
  3076. # Author: Mark Martinec <Mark.Martinec@ijs.si>, 2002
  3077. #
  3078. sub expand($$) {
  3079. my($str_ref) = shift; # a ref to a source string to be macro expanded;
  3080. my($builtins_href) = shift; # a hashref, mapping builtin macro names (single
  3081. # char) to macro values: strings or array refs
  3082. my($lex_lbr, $lex_lbrq, $lex_rbr, $lex_sep, $lex_h) =
  3083. \('[', '[?', ']', '|', '#'); # lexical elements to be used as references
  3084. my(%lexmap); # maps string to reference in order to protect lexels
  3085. for (keys(%$builtins_href))
  3086. { $lexmap{"%$_"} = \"%$_"; $lexmap{"%#$_"} = \"%#$_" }
  3087. for ($lex_lbr, $lex_lbrq, $lex_rbr, $lex_sep, $lex_h) { $lexmap{$$_} = $_ }
  3088. # parse lexically
  3089. my(@tokens) = $$str_ref =~ /\G \# | \[\?? | [\]|] | % \#? . | \\ [^0-7] |
  3090. \\ [0-7]{1,3} | [^\[\]\\|%\n#]+ | [^\n]+? | \n /gcsx;
  3091. # replace lexical element strings with object references,
  3092. # unquote backslash-quoted characters and %%, and drop backslash-newlines
  3093. my(%esc) = (r => "\r", n => "\n", f => "\f", b => "\b",
  3094. e => "\e", a => "\a", t => "\t");
  3095. for (@tokens) {
  3096. if (exists $lexmap{$_}) { $_ = $lexmap{$_} } # replace with refs
  3097. elsif ($_ eq "\\\n") { $_ = '' } # drop \NEWLINE
  3098. elsif (/^%(%)\z/) { $_ = $1 } # %% -> %
  3099. elsif (/^(%#?.)\z/s) { $_ = \$1 } # unknown builtins
  3100. elsif (/^\\([0-7]{1,3})\z/) { $_ = chr(oct($1)) } # \nnn
  3101. elsif (/^\\(.)\z/s) { $_ = (exists($esc{$1}) ? $esc{$1} : $1) }
  3102. }
  3103. my($call_level) = 0; my($quote_level) = 0; my(@macro_type, @arg);
  3104. my(%builtins_cached); my($output_str) = ''; my($whereto) = \$output_str;
  3105. while (@tokens) {
  3106. my($t) = shift(@tokens);
  3107. if ($t eq '') { # ignore leftovers
  3108. } elsif ($quote_level>0 && ref($t) && ($t == $lex_lbr || $t == $lex_lbrq)){
  3109. $quote_level++;
  3110. ref($whereto) eq 'ARRAY' ? push(@$whereto, $t) : ($$whereto .= $t);
  3111. } elsif (ref($t) && $t == $lex_lbr) { # begin iterator macro call
  3112. $quote_level++; $call_level++;
  3113. unshift(@arg, [[]]); unshift(@macro_type, ''); $whereto = $arg[0][0];
  3114. } elsif (ref($t) && $t == $lex_lbrq) { # begin selector macro call
  3115. $call_level++; unshift(@arg, [[]]); unshift(@macro_type, '');
  3116. $whereto = $arg[0][0]; $macro_type[0] = 'select';
  3117. } elsif ($quote_level > 1 && ref($t) && $t == $lex_rbr) {
  3118. $quote_level--;
  3119. ref($whereto) eq 'ARRAY' ? push(@$whereto, $t) : ($$whereto .= $t);
  3120. } elsif ($call_level > 0 && ref($t) && $t == $lex_sep) { # next argument
  3121. if ($quote_level == 0 && $macro_type[0] eq 'select' && @{$arg[0]} == 1) {
  3122. $quote_level++;
  3123. }
  3124. if ($quote_level == 1) {
  3125. unshift(@{$arg[0]}, []); $whereto = $arg[0][0]; # begin next arg
  3126. } else {
  3127. ref($whereto) eq 'ARRAY' ? push(@$whereto, $t) : ($$whereto .= $t);
  3128. }
  3129. } elsif ($quote_level > 0 && ref($t) && $t == $lex_rbr) {
  3130. $quote_level--; # quote level just dropped to 0, this is now a call
  3131. $call_level-- if $call_level > 0;
  3132. my(@result);
  3133. if ($macro_type[0] eq 'select') {
  3134. my($sel, @alternatives) = reverse @{$arg[0]}; # list of refs
  3135. # turn reference into a string, avoid warnings about uninitialized val.
  3136. $sel = !ref($sel) ? '' : join('', map {defined $_ ? $_ : ''} @$sel);
  3137. if ($sel =~ /^\s*\z/) { $sel = 0 }
  3138. elsif ($sel =~ /^\s*(\d+)\s*\z/) { $sel = 0+$1 } # make numeric
  3139. else { $sel = 1 }
  3140. # provide an empty second alternative if we only have one specified
  3141. push(@alternatives, []) if @alternatives < 2 && $sel > 0;
  3142. if ($sel < 0) { $sel = 0 }
  3143. elsif ($sel > $#alternatives) { $sel = $#alternatives }
  3144. @result = @{$alternatives[$sel]};
  3145. } else { # iterator
  3146. my($cvar_r, $sep_r, $body_r, $cvar); # give meaning to arguments
  3147. if (@{$arg[0]} >= 3) { ($cvar_r,$body_r,$sep_r) = reverse @{$arg[0]} }
  3148. else { ($body_r, $sep_r) = reverse @{$arg[0]}; $cvar_r = $body_r }
  3149. # find the formal argument name (iterator)
  3150. for (@$cvar_r) {
  3151. if (ref && $$_ =~ /^%(.)\z/s) { $cvar = $1; last }
  3152. }
  3153. if (exists($builtins_href->{$cvar})) {
  3154. my($values_r);
  3155. if (exists($builtins_cached{$cvar})) {
  3156. $values_r = $builtins_cached{$cvar};
  3157. } else {
  3158. $values_r = $builtins_href->{$cvar};
  3159. while (ref($values_r) eq 'CODE') { $values_r = &$values_r }
  3160. $builtins_cached{$cvar} = $values_r;
  3161. }
  3162. $values_r = [$values_r] if !ref($values_r);
  3163. my($ind);
  3164. my($re) = qr/^%\Q$cvar\E\z/;
  3165. for my $val (@$values_r) {
  3166. push(@result, @$sep_r) if ++$ind > 1 && ref($sep_r);
  3167. push(@result, map { (ref && $$_ =~ /$re/) ? $val : $_ } @$body_r);
  3168. }
  3169. }
  3170. }
  3171. shift(@macro_type); # pop the call stack
  3172. shift(@arg);
  3173. $whereto = $call_level > 0 ? $arg[0][0] : \$output_str;
  3174. unshift(@tokens, @result); # active macro call, evaluate result
  3175. } else { # quoted, plain string, simple macro call, or a misplaced token
  3176. my($s) = '';
  3177. if ($quote_level > 0 || !ref($t)) {
  3178. $s = $t; # quoted or string
  3179. } elsif ($t == $lex_h) { # discard tokens to (and including) newline
  3180. while (@tokens) { last if shift(@tokens) eq "\n" }
  3181. } elsif ($$t =~ /^%(\#)?(.)\z/s) { # macro call %#x or %x
  3182. my($num,$m) = ($1,$2);
  3183. if (!exists($builtins_href->{$m})) { $s = '' } # no such
  3184. elsif (exists($builtins_cached{$m})) { $s = $builtins_cached{$m} }
  3185. else {
  3186. $s = $builtins_href->{$m};
  3187. while (ref($s) eq 'CODE') { $s = &$s } # subroutine callback
  3188. $builtins_cached{$m} = $s;
  3189. }
  3190. if (defined $num && $num eq '#') { # macro call form %#x
  3191. # for array: number of elements; for scalar: nonwhite=1, other 0
  3192. $s = ref($s) ? @$s : $s !~ /^\s*\z/ ? 1 : 0;
  3193. } else { # macro call %x evaluates to the value of macro x
  3194. $s = join(', ', @$s) if ref $s;
  3195. }
  3196. } else { $s = $$t } # misplaced token, e.g. a top level | or ]
  3197. ref($whereto) eq 'ARRAY' ? push(@$whereto, $s) : ($$whereto .= $s);
  3198. }
  3199. }
  3200. \$output_str;
  3201. }
  3202. 1;
  3203. #
  3204. package Amavis::IO::Zlib;
  3205. # A simple IO::File -compatible wrapper around Compress::Zlib,
  3206. # much like IO::Zlib but simpler: does only what we need and does it carefully
  3207. use strict;
  3208. use re 'taint';
  3209. BEGIN {
  3210. use Exporter ();
  3211. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  3212. $VERSION = '2.043';
  3213. @ISA = qw(Exporter);
  3214. }
  3215. use Errno qw(EIO);
  3216. use Compress::Zlib;
  3217. sub new {
  3218. my($class) = shift; my($self) = bless {}, $class;
  3219. if (@_) { $self->open(@_) or return undef }
  3220. $self;
  3221. }
  3222. sub close {
  3223. my($self) = shift;
  3224. my($status); eval { $status = $self->{fh}->gzclose }; delete $self->{fh};
  3225. if ($status != Z_OK || $@ ne '') {
  3226. die "gzclose error: $gzerrno"; # can't stash arbitrary text into $!
  3227. $! = EIO; return undef; # not reached
  3228. }
  3229. 1;
  3230. }
  3231. sub DESTROY {
  3232. my($self) = shift;
  3233. if (ref $self && $self->{fh}) { eval { $self->close } }
  3234. }
  3235. sub open {
  3236. my($self,$fname,$mode) = @_;
  3237. delete $self->{fh};
  3238. $self->{fname} = $fname; $self->{mode} = $mode; $self->{pos} = 0;
  3239. my($gz) = gzopen($fname,$mode);
  3240. if ($gz) { $self->{fh} = $gz }
  3241. else {
  3242. die "gzopen error: $gzerrno"; # can't stash arbitrary text into $!
  3243. $! = EIO; undef $gz; # not reached
  3244. }
  3245. $gz;
  3246. }
  3247. sub seek {
  3248. my($self,$pos,$whence) = @_;
  3249. $whence==0 && $pos==0
  3250. or die "Seek to $whence,$pos on gzipped file not supported";
  3251. $self->{mode} eq 'rb'
  3252. or die "Seek to $whence,$pos on gzipped file only supported for 'rb' mode";
  3253. if ($self->{pos}==0) { 1 } # already there
  3254. else { $self->close; $self->open($self->{fname},$self->{mode}) }
  3255. }
  3256. sub read { # SCALAR,LENGTH,OFFSET
  3257. my($self) = shift; $self->{pos} = 1;
  3258. !defined($_[2]) || $_[2]==0
  3259. or die "Reading gzipped file to an offset not supported";
  3260. my($nbytes) = $self->{fh}->gzread($_[0], defined $_[1] ? $_[1] : 4096);
  3261. if ($nbytes < 0) {
  3262. die "gzread error: $gzerrno"; # can't stash arbitrary text into $!
  3263. $! = EIO; undef $nbytes; # not reached
  3264. }
  3265. $nbytes; # eof: 0; error: undef
  3266. }
  3267. sub getline {
  3268. my($self) = shift; $self->{pos} = 1; my($nbytes,$line);
  3269. $nbytes = $self->{fh}->gzreadline($line);
  3270. if ($nbytes <= 0) { # eof (0) or error (-1)
  3271. $! = 0; undef $line;
  3272. if ($nbytes < 0 && $gzerrno != Z_STREAM_END) {
  3273. die "gzreadline error: $gzerrno"; # can't stash arbitrary text into $!
  3274. $! = EIO; # not reached
  3275. }
  3276. }
  3277. $line; # eof: undef, $! zero; error: undef, $! nonzero
  3278. }
  3279. sub print {
  3280. my($self) = shift;
  3281. my($nbytes); my($len) = length($_[0]);
  3282. if ($len <= 0) { $nbytes = "0 but true" }
  3283. else {
  3284. $self->{pos} = 1; $nbytes = $self->{fh}->gzwrite($_[0]);
  3285. if ($nbytes <= 0) {
  3286. die "gzwrite error: $gzerrno"; # can't stash arbitrary text into $!
  3287. $! = EIO; undef $nbytes; # not reached
  3288. }
  3289. }
  3290. $nbytes;
  3291. }
  3292. sub printf { shift->print(sprintf(shift,@_)) }
  3293. 1;
  3294. #
  3295. package Amavis::In::Connection;
  3296. # Keeps relevant information about how we received the message:
  3297. # client connection information, SMTP envelope and SMTP parameters
  3298. use strict;
  3299. use re 'taint';
  3300. BEGIN {
  3301. use Exporter ();
  3302. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  3303. $VERSION = '2.043';
  3304. @ISA = qw(Exporter);
  3305. }
  3306. sub new
  3307. { my($class) = @_; bless {}, $class }
  3308. sub client_ip # client IP address (immediate SMTP client, i.e. our MTA)
  3309. { my($self)=shift; !@_ ? $self->{client_ip} : ($self->{client_ip}=shift) }
  3310. sub socket_ip # IP address of our interface that received connection
  3311. { my($self)=shift; !@_ ? $self->{socket_ip} : ($self->{socket_ip}=shift) }
  3312. sub socket_port # TCP port of our interface that received connection
  3313. { my($self)=shift; !@_ ? $self->{socket_port}:($self->{socket_port}=shift) }
  3314. sub proto # TCP/UNIX
  3315. { my($self)=shift; !@_ ? $self->{proto} : ($self->{proto}=shift) }
  3316. sub smtp_proto # SMTP/ESMTP(A|S|SA)/LMTP(A|S|SA) # rfc3848, or QMQP/QMQPqq
  3317. { my($self)=shift; !@_ ? $self->{smtp_proto}: ($self->{smtp_proto}=shift) }
  3318. sub smtp_helo # (E)SMTP HELO/EHLO parameter
  3319. { my($self)=shift; !@_ ? $self->{smtp_helo} : ($self->{smtp_helo}=shift) }
  3320. 1;
  3321. #
  3322. package Amavis::In::Message::PerRecip;
  3323. use strict;
  3324. use re 'taint';
  3325. BEGIN {
  3326. use Exporter ();
  3327. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  3328. $VERSION = '2.043';
  3329. @ISA = qw(Exporter);
  3330. }
  3331. # per-recipient data are kept in an array of n-tuples:
  3332. # (recipient-address, destiny, done, smtp-response-text, remote-mta, ...)
  3333. sub new # NOTE: this class is a list for historical reasons, not a hash
  3334. { my($class) = @_; bless [(undef) x 15], $class }
  3335. # subs to set or access individual elements of a n-tuple by name
  3336. sub recip_addr # raw (unquoted) recipient envelope e-mail address
  3337. { my($self)=shift; !@_ ? $$self[0] : ($$self[0]=shift) }
  3338. sub recip_addr_modified # recip. addr. with possible addr. extension inserted
  3339. { my($self)=shift; !@_ ? $$self[1] : ($$self[1]=shift) }
  3340. sub recip_destiny # D_REJECT, D_BOUNCE, D_DISCARD, D_PASS
  3341. { my($self)=shift; !@_ ? $$self[2] : ($$self[2]=shift) }
  3342. sub recip_done # false: not done, true: done (1: faked, 2: truly sent)
  3343. { my($self)=shift; !@_ ? $$self[3] : ($$self[3]=shift) }
  3344. sub recip_smtp_response # rfc2821 response (3-digit + enhanced resp + text)
  3345. { my($self)=shift; !@_ ? $$self[4] : ($$self[4]=shift) }
  3346. sub recip_remote_mta_smtp_response # smtp response as issued by remote MTA
  3347. { my($self)=shift; !@_ ? $$self[5] : ($$self[5]=shift) }
  3348. sub recip_remote_mta # remote MTA that issued the smtp response
  3349. { my($self)=shift; !@_ ? $$self[6] : ($$self[6]=shift) }
  3350. sub recip_mbxname # mailbox name or file when known (local:, bsmtp: or sql:)
  3351. { my($self)=shift; !@_ ? $$self[7] : ($$self[7]=shift) }
  3352. sub recip_whitelisted_sender # recip considers this sender whitelisted (> 0)
  3353. { my($self)=shift; !@_ ? $$self[8] : ($$self[8]=shift) }
  3354. sub recip_blacklisted_sender # recip considers this sender blacklisted
  3355. { my($self)=shift; !@_ ? $$self[9] : ($$self[9]=shift) }
  3356. sub recip_score_boost # recip adds penalty spam points to the final score
  3357. { my($self)=shift; !@_ ? $$self[10] : ($$self[10]=shift) }
  3358. sub infected # contains a virus (1); check bypassed (undef); clean (0)
  3359. { my($self)=shift; !@_ ? $$self[11] : ($$self[11]=shift) }
  3360. sub banned_parts # banned part descriptions (ref to a list of banned parts)
  3361. { my($self)=shift; !@_ ? $$self[12] : ($$self[12]=shift) }
  3362. sub banned_keys # keys of matching banned rules (a ref to a list)
  3363. { my($self)=shift; !@_ ? $$self[13] : ($$self[13]=shift) }
  3364. sub banned_rhs # right-hand side of matching rules (a ref to a list)
  3365. { my($self)=shift; !@_ ? $$self[14] : ($$self[14]=shift) }
  3366. sub recip_final_addr { # return recip_addr_modified if set, else recip_addr
  3367. my($self)=shift;
  3368. my($newaddr) = $self->recip_addr_modified;
  3369. defined $newaddr ? $newaddr : $self->recip_addr;
  3370. }
  3371. 1;
  3372. #
  3373. package Amavis::In::Message;
  3374. # this class contains information about the message being processed
  3375. use strict;
  3376. use re 'taint';
  3377. BEGIN {
  3378. use Exporter ();
  3379. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  3380. $VERSION = '2.043';
  3381. @ISA = qw(Exporter);
  3382. }
  3383. BEGIN {
  3384. import Amavis::Conf qw(:platform);
  3385. import Amavis::rfc2821_2822_Tools qw(rfc2822_timestamp);
  3386. import Amavis::In::Message::PerRecip;
  3387. }
  3388. sub new
  3389. { my($class) = @_; bless {}, $class }
  3390. sub rx_time # Unix time (s since epoch) of message reception by amavisd
  3391. { my($self)=shift; !@_ ? $self->{rx_time} : ($self->{rx_time}=shift) }
  3392. sub client_addr # original client IP addr, obtained from XFORWARD or milter
  3393. { my($self)=shift; !@_ ? $self->{cli_ip} : ($self->{cli_ip}=shift) }
  3394. sub client_name # orig. client DNS name, obtained from XFORWARD or milter
  3395. { my($self)=shift; !@_ ? $self->{cli_name} : ($self->{cli_name}=shift) }
  3396. sub client_proto # orig. client protocol, obtained from XFORWARD or milter
  3397. { my($self)=shift; !@_ ? $self->{cli_proto} : ($self->{cli_proto}=shift) }
  3398. sub client_helo # orig. client EHLO name, obtained from XFORWARD or milter
  3399. { my($self)=shift; !@_ ? $self->{cli_helo} : ($self->{cli_helo}=shift) }
  3400. sub queue_id # MTA queue ID of message if known (Courier, milter/AM.PDP)
  3401. { my($self)=shift; !@_ ? $self->{queue_id} : ($self->{queue_id}=shift) }
  3402. sub mail_id # some long-term unique id of the message on this system
  3403. { my($self)=shift; !@_ ? $self->{mail_id} : ($self->{mail_id}=shift) }
  3404. sub secret_id # secret string to grant access to message with mail_id
  3405. { my($self)=shift; !@_ ? $self->{secret_id} : ($self->{secret_id}=shift) }
  3406. sub msg_size # ESMTP SIZE value, later corrected by actual message size
  3407. { my($self)=shift; !@_ ? $self->{msg_size} : ($self->{msg_size}=shift) }
  3408. sub auth_user # ESMTP AUTH username
  3409. { my($self)=shift; !@_ ? $self->{auth_user} : ($self->{auth_user}=shift) }
  3410. sub auth_pass # ESMTP AUTH password
  3411. { my($self)=shift; !@_ ? $self->{auth_pass} : ($self->{auth_pass}=shift) }
  3412. sub auth_submitter # ESMTP MAIL command AUTH option value (addr-spec or "<>")
  3413. { my($self)=shift; !@_ ? $self->{auth_subm} : ($self->{auth_subm}=shift) }
  3414. sub requested_by # Resent-From addr who requested release from a quarantine
  3415. { my($self)=shift; !@_ ? $self->{requested_by}:($self->{requested_by}=shift)}
  3416. sub body_type # ESMTP BODY param (rfc1652: 7BIT, 8BITMIME) or BINARYMIME
  3417. { my($self)=shift; !@_ ? $self->{body_type} : ($self->{body_type}=shift) }
  3418. sub sender # envelope sender
  3419. { my($self)=shift; !@_ ? $self->{sender} : ($self->{sender}=shift) }
  3420. sub sender_contact # unmangled sender address or undef (e.g. believed faked)
  3421. { my($self)=shift; !@_ ? $self->{sender_c} : ($self->{sender_c}=shift) }
  3422. sub sender_source # unmangled sender address or info from the trace
  3423. { my($self)=shift; !@_ ? $self->{sender_src} : ($self->{sender_src}=shift) }
  3424. sub mime_entity # MIME::Parser entity holding the message
  3425. { my($self)=shift; !@_ ? $self->{mime_entity}: ($self->{mime_entity}=shift)}
  3426. sub parts_root # Amavis::Unpackers::Part root object
  3427. { my($self)=shift; !@_ ? $self->{parts_root}: ($self->{parts_root}=shift)}
  3428. sub mail_text # rfc2822 msg: (open) file handle, or MIME::Entity object
  3429. { my($self)=shift; !@_ ? $self->{mail_text} : ($self->{mail_text}=shift) }
  3430. sub mail_text_fn # orig. mail filename or undef, e.g. mail_tempdir/email.txt
  3431. { my($self)=shift; !@_ ? $self->{mail_text_fn} : ($self->{mail_text_fn}=shift) }
  3432. sub mail_tempdir # work directory, under $TEMPBASE or supplied by client
  3433. { my($self)=shift; !@_ ? $self->{mail_tempdir} : ($self->{mail_tempdir}=shift) }
  3434. sub header_edits # Amavis::Out::EditHeader object or undef
  3435. { my($self)=shift; !@_ ? $self->{hdr_edits} : ($self->{hdr_edits}=shift) }
  3436. sub orig_header # original header - an arrayref of lines, with trailing LF
  3437. { my($self)=shift; !@_ ? $self->{orig_header}: ($self->{orig_header}=shift) }
  3438. sub orig_header_size # size of original header
  3439. { my($self)=shift; !@_ ? $self->{orig_hdr_s} : ($self->{orig_hdr_s}=shift) }
  3440. sub orig_body_size # size of original body
  3441. { my($self)=shift; !@_ ? $self->{orig_bdy_s} : ($self->{orig_bdy_s}=shift) }
  3442. sub body_digest # message digest of a message body (e.g. MD5 or SHA1)
  3443. { my($self)=shift; !@_ ? $self->{body_digest}: ($self->{body_digest}=shift) }
  3444. sub quarantined_to # list of quarantine mailbox names or addresses if quarantined
  3445. { my($self)=shift; !@_ ? $self->{quarantine} : ($self->{quarantine}=shift) }
  3446. sub quar_type # quarantine type: F/Z/B/Q/M (file/zipfile/bsmtp/sql/mailbox)
  3447. { my($self)=shift; !@_ ? $self->{quar_type} : ($self->{quar_type}=shift) }
  3448. sub dsn_sent # delivery status notification was sent(1) or faked(2)
  3449. { my($self)=shift; !@_ ? $self->{dsn_sent} : ($self->{dsn_sent}=shift) }
  3450. sub delivery_method # delivery method, or empty for implicit delivery (milter)
  3451. { my($self)=shift; !@_ ? $self->{delivery_method}:($self->{delivery_method}=shift)}
  3452. sub client_delete # don't delete the tempdir, it is a client's reponsibility
  3453. { my($self)=shift; !@_ ? $self->{client_delete}:($self->{client_delete}=shift)}
  3454. # credativ -jw
  3455. sub postfixid # the original postfix queue id
  3456. { my($self)=shift; !@_ ? $self->{postfixid} : ($self->{postfixid}=shift) }
  3457. # credativ end
  3458. # The order of entries in the list is the original order in which
  3459. # recipient addresses (e.g. obtained via 'MAIL TO:') were received.
  3460. # Only the entries that were accepted (via SMTP response code 2xx)
  3461. # are placed in the list. The ORDER MUST BE PRESERVED and no recipients
  3462. # may be added or removed from the list! This is vital to be able
  3463. # to produce correct per-recipient responses to a LMTP client!
  3464. #
  3465. sub per_recip_data { # get or set a listref of envelope recipient n-tuples
  3466. my($self) = shift;
  3467. # store a given listref of n-tuples (originals, not copies!)
  3468. if (@_) { @{$self->{recips}} = @{$_[0]} }
  3469. # return a listref to the original n-tuples,
  3470. # caller may modify the data if he knows what he is doing
  3471. $self->{recips};
  3472. }
  3473. sub recips { # get or set a listref of envelope recipients
  3474. my($self)=shift;
  3475. if (@_) { # store a copy of a given listref of recipient addresses
  3476. # wrap scalars (strings) into n-tuples
  3477. $self->per_recip_data([ map {
  3478. my($per_recip_obj) = Amavis::In::Message::PerRecip->new;
  3479. $per_recip_obj->recip_addr($_);
  3480. $per_recip_obj->recip_destiny(D_PASS); # default is Pass
  3481. $per_recip_obj } @{$_[0]} ]);
  3482. }
  3483. return if !defined wantarray; # don't bother
  3484. # return listref of recipient addresses
  3485. [ map { $_->recip_addr } @{$self->per_recip_data} ];
  3486. }
  3487. 1;
  3488. #
  3489. package Amavis::Out::EditHeader;
  3490. # Accumulates instructions on what lines need to be added to the message
  3491. # header, deleted, or how to change existing lines, then via a call
  3492. # to write_header() performs these edits on the fly.
  3493. use strict;
  3494. use re 'taint';
  3495. BEGIN {
  3496. use Exporter ();
  3497. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  3498. $VERSION = '2.043';
  3499. @ISA = qw(Exporter);
  3500. @EXPORT_OK = qw(&hdr);
  3501. }
  3502. BEGIN {
  3503. import Amavis::Conf qw(:platform c cr ca);
  3504. import Amavis::Timing qw(section_time);
  3505. import Amavis::Util qw(ll do_log safe_encode q_encode);
  3506. }
  3507. use MIME::Words;
  3508. sub new { my($class) = @_; bless {}, $class }
  3509. sub prepend_header($$$;$) {
  3510. my($self, $field_name, $field_body, $structured) = @_;
  3511. unshift(@{$self->{prepend}}, hdr($field_name, $field_body, $structured));
  3512. }
  3513. sub append_header($$$;$) {
  3514. my($self, $field_name, $field_body, $structured) = @_;
  3515. push(@{$self->{append}}, hdr($field_name, $field_body, $structured));
  3516. }
  3517. sub delete_header($$) {
  3518. my($self, $field_name) = @_;
  3519. $self->{edit}{lc($field_name)} = undef;
  3520. }
  3521. sub edit_header($$$;$) {
  3522. my($self, $field_name, $field_edit_sub, $structured) = @_;
  3523. # $field_edit_sub will be called with 2 args: field name and field body;
  3524. # it should return the replacement field body (no field name and colon),
  3525. # with or without the trailing NL
  3526. !defined($field_edit_sub) || ref($field_edit_sub) eq 'CODE'
  3527. or die "edit_header: arg#3 must be undef or a subroutine ref";
  3528. $self->{edit}{lc($field_name)} = $field_edit_sub;
  3529. }
  3530. # copy all header edits from another header-edits object into this one
  3531. sub inherit_header_edits($$) {
  3532. my($self, $other_edits) = @_;
  3533. if (defined $other_edits) {
  3534. unshift(@{$self->{prepend}},
  3535. @{$other_edits->{prepend}}) if $other_edits->{prepend};
  3536. unshift(@{$self->{append}},
  3537. @{$other_edits->{append}}) if $other_edits->{append};
  3538. if ($other_edits->{edit}) {
  3539. for (keys %{$other_edits->{edit}})
  3540. { $self->{edit}{$_} = $other_edits->{edit}{$_} }
  3541. }
  3542. }
  3543. }
  3544. # Insert space after colon if not present, RFC2047-encode if field body
  3545. # contains non-ASCII characters, fold long lines if needed,
  3546. # prepend space before each NL if missing, append NL if missing;
  3547. # Header fields with only spaces are not allowed.
  3548. # (rfc2822: Each line of characters MUST be no more than 998 characters,
  3549. # and SHOULD be no more than 78 characters, excluding the CRLF.
  3550. # '$structured' indicates that folding is only allowed at positions
  3551. # indicated by \n in the provided header body.
  3552. #
  3553. sub hdr($$;$) {
  3554. my($field_name, $field_body, $structured) = @_;
  3555. if ($field_name =~ /^(X-.*|Subject|Comments)\z/si &&
  3556. $field_body =~ /[^\011\012\040-\176]/ #any nonprintable except TAB and LF
  3557. ) { # encode according to RFC 2047
  3558. $field_body =~ s/\n([ \t])/$1/g; # unfold
  3559. chomp($field_body);
  3560. my($field_body_octets) = safe_encode(c('hdr_encoding'), $field_body);
  3561. my($qb) = c('hdr_encoding_qb');
  3562. if (uc($qb) eq 'Q') {
  3563. $field_body = q_encode($field_body_octets, $qb, c('hdr_encoding'));
  3564. } else {
  3565. $field_body = MIME::Words::encode_mimeword($field_body_octets,
  3566. $qb, c('hdr_encoding'));
  3567. }
  3568. } else { # supposed to be in plain ASCII, let's make sure it is
  3569. $field_body = safe_encode('ascii', $field_body);
  3570. }
  3571. $field_name = safe_encode('ascii', $field_name);
  3572. my($str) = $field_name . ':';
  3573. $str .= ' ' if $field_body !~ /^[ \t]/;
  3574. $str .= $field_body;
  3575. $str =~ s/\n([^ \t\n])/\n $1/g; # insert a space at line folds if missing
  3576. $str =~ s/\n([ \t]*\n)+/\n/g; # remove empty lines
  3577. chomp($str); # chop off trailing NL if present
  3578. if ($structured) {
  3579. $str =~ s/[ \t]+/ /g; # collapse spaces and tabs to a single space
  3580. my(@sublines) = split(/\n/, $str, -1);
  3581. $str = ''; my($s) = ''; my($s_l) = 0; my($s_il)=0;
  3582. for (@sublines) { # join shorter field sections
  3583. if ($s !~ /^\s*\z/ && $s_l + $s_il + length($_) > 78) {
  3584. $s_il = 8; # length of the initial tab
  3585. $str .= "\n\t" if $str ne '';
  3586. $s =~ s/^[ \t]+//g; # remove leading and trailing whitespace
  3587. $s =~ s/[ \t]+$//g;
  3588. $str .= $s; $s = ''; $s_l = 0;
  3589. }
  3590. $s .= $_; $s_l += length($_);
  3591. }
  3592. if ($s !~ /^\s*\z/) {
  3593. $str .= "\n\t" if $str ne '';
  3594. $s =~ s/^[ \t]+//g; # remove leading and trailing whitespace
  3595. $s =~ s/[ \t]+$//g;
  3596. $str .= $s;
  3597. }
  3598. } elsif (length($str) > 998) {
  3599. # truncate the damn thing (to be done better)
  3600. $str = substr($str,0,998);
  3601. }
  3602. $str .= "\n"; # append final NL
  3603. do_log(5, "header: $str");
  3604. $str;
  3605. }
  3606. # Copy mail header to the supplied method (line by line) while adding,
  3607. # removing, or changing certain header lines as required, and append
  3608. # an empty line (end-of-header). Returns number of original 'Received:'
  3609. # header fields to make simple loop detection possible (as required
  3610. # by rfc2821 section 6.2).
  3611. #
  3612. # Assumes input file is properly positioned, leaves it positioned
  3613. # at the beginning of the body.
  3614. #
  3615. sub write_header($$$) {
  3616. my($self, $msg, $out_fh) = @_;
  3617. my($is_mime) = ref($msg) && $msg->isa('MIME::Entity') ? 1 : 0;
  3618. do_log(5,"write_header: $is_mime, $out_fh");
  3619. $out_fh = IO::Wrap::wraphandle($out_fh); # assure an IO::Handle-like obj
  3620. my(@header);
  3621. if ($is_mime) {
  3622. @header = map { /^[ \t]*\n?\z/ ? () # remove empty lines, ensure NL
  3623. : (/\n\z/ ? $_ : $_ . "\n") } @{$msg->header};
  3624. }
  3625. my($received_cnt) = 0; my($str) = '';
  3626. for (@{$self->{prepend}}) { $str .= $_ }
  3627. if ($str ne '') { $out_fh->print($str) or die "sending mail header1: $!" }
  3628. if (!defined($msg)) {
  3629. # existing header empty
  3630. } else {
  3631. push(@header, $eol) if $is_mime; # append empty line as end-of-header
  3632. local($1,$2); my($curr_head,$next_head); my($illcnt) = 0; undef $!;
  3633. while (defined($next_head = $is_mime ? shift @header : $msg->getline)) {
  3634. if ($next_head =~ /^[ \t]/) { $curr_head .= $next_head } # folded
  3635. else { # new header
  3636. if (!defined($curr_head)) { # no previous complete header field
  3637. } elsif ($curr_head !~ /^([!-9;-\176]+)[ \t]*:(.*)\z/s) {
  3638. # invalid header, but we don't care
  3639. $curr_head =~ s{\n [ \t]* (?= \n )}{}gsx and $illcnt++;
  3640. $out_fh->print($curr_head) or die "sending mail header4: $!";
  3641. } else { # count, edit, or delete
  3642. # obsolete rfc822 syntax allowed whitespace before colon
  3643. my($field_name, $field_body) = ($1, $2);
  3644. my($field_name_lc) = lc($field_name);
  3645. $received_cnt++ if $field_name_lc eq 'received';
  3646. if (!exists($self->{edit}{$field_name_lc})) { # unchanged
  3647. # unfold illegal all-whitespace continuation lines
  3648. $curr_head =~ s{\n [ \t]* (?= \n )}{}gsx and $illcnt++;
  3649. $out_fh->print($curr_head) or die "sending mail header5: $!";
  3650. } else {
  3651. my($edit) = $self->{edit}{$field_name_lc};
  3652. if (defined($edit)) { # edit, not delete
  3653. chomp($field_body);
  3654. ### $field_body =~ s/\n([ \t])/$1/g; # unfold
  3655. my($subst) = hdr($field_name, &$edit($field_name,$field_body));
  3656. $subst =~ s{\n [ \t]* (?= \n )}{}gsx and $illcnt++;
  3657. $out_fh->print($subst) or die "sending mail header6: $!";
  3658. }
  3659. }
  3660. }
  3661. last if $next_head eq $eol; # end-of-header reached
  3662. $curr_head = $next_head;
  3663. }
  3664. undef $!;
  3665. }
  3666. defined $next_head || $is_mime || $!==0
  3667. or die "Error reading mail header: $!";
  3668. do_log(0, "INFO: unfolded $illcnt illegal all-whitespace ".
  3669. "continuation lines") if $illcnt;
  3670. }
  3671. $str = '';
  3672. for (@{$self->{append}}) { $str .= $_ }
  3673. $str .= $eol; # end of header - separator line
  3674. $out_fh->print($str) or die "sending mail header7: $!";
  3675. section_time('write-header');
  3676. $received_cnt;
  3677. }
  3678. 1;
  3679. #
  3680. package Amavis::Out::Local;
  3681. use strict;
  3682. use re 'taint';
  3683. BEGIN {
  3684. use Exporter ();
  3685. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  3686. $VERSION = '2.043';
  3687. @ISA = qw(Exporter);
  3688. @EXPORT_OK = qw(&mail_to_local_mailbox);
  3689. }
  3690. use Errno qw(ENOENT EACCES);
  3691. use IO::File qw(O_CREAT O_EXCL O_WRONLY);
  3692. use IO::Wrap;
  3693. BEGIN {
  3694. import Amavis::Conf qw(:platform $quarantine_subdir_levels c cr ca);
  3695. import Amavis::Lock;
  3696. import Amavis::Util qw(ll do_log am_id exit_status_str run_command_consumer);
  3697. import Amavis::Timing qw(section_time);
  3698. import Amavis::rfc2821_2822_Tools;
  3699. import Amavis::Out::EditHeader;
  3700. }
  3701. use subs @EXPORT_OK;
  3702. # Deliver to local mailboxes only, ignore the rest: either to directory
  3703. # (maildir style), or file (Unix mbox). (normally used as a quarantine method)
  3704. #
  3705. sub mail_to_local_mailbox(@) {
  3706. my($via, $msginfo, $initial_submission, $filter) = @_;
  3707. $via =~ /^local:(.*)\z/si or die "Bad local method: $via";
  3708. my($via_arg) = $1;
  3709. my(@per_recip_data) = grep { !$_->recip_done && (!$filter || &$filter($_)) }
  3710. @{$msginfo->per_recip_data};
  3711. return 1 if !@per_recip_data;
  3712. my($msg) = $msginfo->mail_text; # a file handle or a MIME::Entity object
  3713. if (defined($msg) && !$msg->isa('MIME::Entity')) {
  3714. # at this point, we have no idea what the user gave us...
  3715. # a globref? a FileHandle?
  3716. $msg = IO::Wrap::wraphandle($msg); # now we have an IO::Handle-like obj
  3717. }
  3718. my($sender) = $msginfo->sender;
  3719. for my $r (@per_recip_data) {
  3720. # each recipient gets its own copy; these are not the original recipients
  3721. my($recip) = $r->recip_final_addr;
  3722. next if $recip eq '';
  3723. my($localpart,$domain) = split_address($recip);
  3724. my($smtp_response);
  3725. # %local_delivery_aliases emulates aliases map - this would otherwise
  3726. # be done by MTA's local delivery agent if we gave the message to MTA.
  3727. # This way we keep interface compatible with other mail delivery
  3728. # methods. The hash value may be a ref to a pair of fixed strings,
  3729. # or a subroutine ref (which must return such pair) to allow delayed
  3730. # (lazy) evaluation when some part of the pair is not yet known
  3731. # at initialization time.
  3732. # If no matching entry is found, the key ($localpart) is treated as
  3733. # a mailbox filename if nonempty, or else quarantining is skipped.
  3734. my($mbxname, $suggested_filename);
  3735. { # a block is used as a 'switch' statement - 'last' will exit from it
  3736. my($ldar) = cr('local_delivery_aliases'); # a ref to a hash
  3737. my($alias) = $ldar->{$localpart};
  3738. if (ref($alias) eq 'ARRAY') {
  3739. ($mbxname, $suggested_filename) = @$alias;
  3740. } elsif (ref($alias) eq 'CODE') { # lazy (delayed) evaluation
  3741. ($mbxname, $suggested_filename) = &$alias;
  3742. } elsif ($alias ne '') {
  3743. ($mbxname, $suggested_filename) = ($alias, undef);
  3744. } elsif (!exists $ldar->{$localpart}) {
  3745. do_log(0, "no key '$localpart' in \%local_delivery_aliases, skip local delivery");
  3746. }
  3747. if ($mbxname eq '') {
  3748. my($why) = !exists $ldar->{$localpart} ? 1 : $alias eq '' ? 2 : 3;
  3749. do_log(2, "skip local delivery($why): <$sender> -> <$recip>");
  3750. $smtp_response = "250 2.6.0 Ok, skip local delivery($why)";
  3751. last; # exit block, not the loop
  3752. }
  3753. my($ux); # is it a UNIX-style mailbox?
  3754. if (!-d $mbxname) { # assume a filename (need not exist yet)
  3755. $ux = 1; # $mbxname is a UNIX-style mailbox (one file)
  3756. } else { # a directory
  3757. $ux = 0; # $mbxname is a directory (amavis/maildir style mailbox)
  3758. my($explicitly_suggested_filename) = $suggested_filename ne '';
  3759. if ($suggested_filename eq '')
  3760. { $suggested_filename = $via_arg ne '' ? $via_arg : '%m' }
  3761. $suggested_filename =~ s{%(.)}
  3762. { $1 eq 'b' ? $msginfo->body_digest
  3763. : $1 eq 'm' ? $msginfo->mail_id
  3764. : $1 eq 'i' ? iso8601_timestamp($msginfo->rx_time,1,'-')
  3765. : $1 eq 'n' ? am_id()
  3766. : $1 eq '%' ? '%' : '%'.$1 }egs;
  3767. $mbxname = "$mbxname/$suggested_filename";
  3768. if ($quarantine_subdir_levels>=1 && !$explicitly_suggested_filename) {
  3769. # using a subdirectory structure to disperse quarantine files
  3770. local($1,$2); my($subdir) = substr($msginfo->mail_id, 0, 1);
  3771. $subdir=~/^[A-Z0-9]\z/i or die "Unexpected first char: $subdir";
  3772. $mbxname =~ m{^ (.*/)? ([^/]+) \z}sx; my($path,$fname) = ($1,$2);
  3773. $mbxname = "$path$subdir/$fname"; # resulting full filename
  3774. my($errn) = stat("$path$subdir") ? 0 : 0+$!;
  3775. if ($errn == ENOENT) { # check/prepare a set of subdirectories
  3776. do_log(2, "checking/creating quarantine subdirs under $path");
  3777. for my $d ('A'..'Z','a'..'z','0'..'9') {
  3778. $errn = stat("$path$d") ? 0 : 0+$!;
  3779. if ($errn == ENOENT) {
  3780. mkdir("$path$d", 0750) or die "Can't create dir $path$d: $!";
  3781. }
  3782. }
  3783. }
  3784. }
  3785. }
  3786. do_log(1, "local delivery: <$sender> -> <$recip>, mbx=$mbxname");
  3787. my($mp,$pos,$pid);
  3788. my($errn) = stat($mbxname) ? 0 : 0+$!;
  3789. local $SIG{CHLD} = 'DEFAULT';
  3790. local $SIG{PIPE} = 'IGNORE'; # write to broken pipe would throw a signal
  3791. eval { # try to open the mailbox file for writing
  3792. if (!$ux) { # one mail per file, will create specified file
  3793. if ($errn == ENOENT) {} # good, no file, as expected
  3794. elsif (!$errn && -f _)
  3795. { die "File $mbxname already exists, refuse to overwrite" }
  3796. else
  3797. { die "File $mbxname exists??? Refuse to overwrite it, $!" }
  3798. if ($mbxname =~ /\.gz\z/) {
  3799. $mp = Amavis::IO::Zlib->new;
  3800. $mp->open($mbxname,'wb')
  3801. or die "Can't create gzip file $mbxname: $!";
  3802. } else {
  3803. $mp = IO::File->new;
  3804. $mp->open($mbxname, O_CREAT|O_EXCL|O_WRONLY, 0640)
  3805. or die "Can't create file $mbxname: $!";
  3806. binmode($mp, ":bytes") or die "Can't cancel :utf8 mode: $!"
  3807. if $unicode_aware;
  3808. }
  3809. } else { # append to UNIX-style mailbox
  3810. # deliver only to non-executable regular files
  3811. if ($errn == ENOENT) {
  3812. $mp = IO::File->new;
  3813. $mp->open($mbxname, O_CREAT|O_EXCL|O_WRONLY, 0640)
  3814. or die "Can't create file $mbxname: $!";
  3815. } elsif (!$errn && !-f _) {
  3816. die "Mailbox $mbxname is not a regular file, refuse to deliver";
  3817. } elsif (-x _ || -X _) {
  3818. die "Mailbox file $mbxname is executable, refuse to deliver";
  3819. } else {
  3820. $mp = IO::File->new;
  3821. $mp->open($mbxname,'>>',0640)
  3822. or die "Can't append to $mbxname: $!";
  3823. }
  3824. binmode($mp, ":bytes") or die "Can't cancel :utf8 mode: $!"
  3825. if $unicode_aware;
  3826. lock($mp);
  3827. $mp->seek(0,2) or die "Can't position mailbox file to its tail: $!";
  3828. $pos = $mp->tell;
  3829. }
  3830. if (defined($msg) && !$msg->isa('MIME::Entity'))
  3831. { $msg->seek(0,0) or die "Can't rewind mail file: $!" }
  3832. };
  3833. if ($@ ne '') {
  3834. chomp($@);
  3835. $smtp_response = $@ eq "timed out" ? "450 4.4.2" : "451 4.5.0";
  3836. $smtp_response .= " Local delivery(1) to $mbxname failed: $@";
  3837. last; # exit block, not the loop
  3838. }
  3839. eval { # if things fail from here on, try to restore mailbox state
  3840. if ($ux) {
  3841. $mp->printf("From %s %s$eol", quote_rfc2821_local($sender),
  3842. scalar(localtime($msginfo->rx_time)) ) # English date!
  3843. or die "Can't write to $mbxname: $!";
  3844. }
  3845. my($hdr_edits) = $msginfo->header_edits;
  3846. if (!$hdr_edits) {
  3847. $hdr_edits = Amavis::Out::EditHeader->new;
  3848. $msginfo->header_edits($hdr_edits);
  3849. }
  3850. $hdr_edits->delete_header('Return-Path');
  3851. $hdr_edits->prepend_header('Delivered-To',
  3852. quote_rfc2821_local($recip));
  3853. $hdr_edits->prepend_header('Return-Path',
  3854. qquote_rfc2821_local($sender));
  3855. my($received_cnt) = $hdr_edits->write_header($msg,$mp);
  3856. if ($received_cnt > 110) {
  3857. # loop detection required by rfc2821 section 6.2
  3858. # Do not modify the signal text, it gets matched elsewhere!
  3859. die "Too many hops: $received_cnt 'Received:' header lines\n";
  3860. }
  3861. if (!$ux) { # do it in blocks for speed if we can
  3862. my($nbytes,$buff);
  3863. while (($nbytes=$msg->read($buff,16384)) > 0)
  3864. { $mp->print($buff) or die "Can't write to $mbxname: $!" }
  3865. defined $nbytes or die "Error reading: $!";
  3866. } else { # for UNIX-style mailbox delivery: escape 'From '
  3867. my($ln); my($blank_line) = 1;
  3868. for (undef $!; defined($ln=$msg->getline); undef $!) {
  3869. $mp->print('>') or die "Can't write to $mbxname: $!"
  3870. if $blank_line && $ln=~/^From /;
  3871. $mp->print($ln) or die "Can't write to $mbxname: $!";
  3872. $blank_line = $ln eq $eol;
  3873. }
  3874. defined $ln || $!==0 or die "Error reading: $!";
  3875. }
  3876. # must append an empty line for a Unix mailbox format
  3877. $mp->print($eol) or die "Can't write to $mbxname: $!" if $ux;
  3878. };
  3879. my($failed) = 0;
  3880. if ($@ ne '') { # trouble
  3881. chomp($@);
  3882. if ($ux && defined($pos) && $can_truncate) {
  3883. # try to restore UNIX-style mailbox to previous size;
  3884. # Produces a fatal error if truncate isn't implemented on the system
  3885. $mp->truncate($pos) or die "Can't truncate file $mbxname: $!";
  3886. }
  3887. $failed = 1;
  3888. }
  3889. unlock($mp) if $ux;
  3890. $mp->close or die "Error closing $mbxname: $!";
  3891. if (!$failed) {
  3892. $smtp_response = "250 2.6.0 Ok, delivered to $mbxname";
  3893. } elsif ($@ eq "timed out") {
  3894. $smtp_response = "450 4.4.2 Local delivery to $mbxname timed out";
  3895. } elsif ($@ =~ /too many hops/i) {
  3896. $smtp_response = "550 5.4.6 Rejected delivery to mailbox $mbxname: $@";
  3897. } else {
  3898. $smtp_response = "451 4.5.0 Local delivery to mailbox $mbxname failed: $@";
  3899. }
  3900. } # end of block, 'last' within block brings us here
  3901. do_log(-1, $smtp_response) if $smtp_response !~ /^2/;
  3902. $smtp_response .= ", id=" . am_id();
  3903. $r->recip_smtp_response($smtp_response); $r->recip_done(2);
  3904. $r->recip_mbxname($mbxname) if $mbxname ne '' && $smtp_response =~ /^2/;
  3905. }
  3906. section_time('save-to-local-mailbox');
  3907. }
  3908. 1;
  3909. #
  3910. package Amavis::Out;
  3911. use strict;
  3912. use re 'taint';
  3913. BEGIN {
  3914. use Exporter ();
  3915. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  3916. $VERSION = '2.043';
  3917. @ISA = qw(Exporter);
  3918. @EXPORT = qw(&mail_dispatch);
  3919. }
  3920. use Errno qw(ENOENT EACCES);
  3921. use IO::File qw(O_CREAT O_EXCL O_WRONLY);
  3922. use IO::Wrap;
  3923. use Net::Cmd;
  3924. use Net::SMTP 2.24;
  3925. # use Authen::SASL;
  3926. use POSIX qw(WIFEXITED WIFSIGNALED WIFSTOPPED
  3927. WEXITSTATUS WTERMSIG WSTOPSIG);
  3928. BEGIN {
  3929. import Amavis::Conf qw(:platform $DEBUG $QUARANTINEDIR
  3930. $relayhost_is_client c cr ca);
  3931. import Amavis::Util qw(untaint min max ll do_log debug_oneshot
  3932. am_id snmp_count exit_status_str
  3933. prolong_timer run_command_consumer);
  3934. import Amavis::Timing qw(section_time);
  3935. import Amavis::rfc2821_2822_Tools;
  3936. import Amavis::Out::Local qw(mail_to_local_mailbox);
  3937. import Amavis::Out::EditHeader;
  3938. }
  3939. # modify delivery method string if $relayhost_is_client and mail came in by TCP
  3940. sub dynamic_destination($$) {
  3941. my($method,$conn) = @_;
  3942. my($client_ip) = !defined($conn) ? undef : $conn->client_ip;
  3943. if ($client_ip ne '' && $method =~ /^smtp:/i) {
  3944. my(@list); $list[0] = ''; my($j) = 0;
  3945. for ($method =~ /\G \[ (?: \\. | [^\]\\] )* \] | " (?: \\. | [^"\\] )* "
  3946. | : | [ \t]+ | [^:"\[ \t]+ | . /gcsx) { # real parsing
  3947. if ($_ eq ':') { $list[++$j] = '' } else { $list[$j] .= $_ }
  3948. };
  3949. my($new_method); my($via,$relayhost,$relayhost_port) = @list;
  3950. if ($relayhost_is_client) # compatibility: deprecated $relayhost_is_client
  3951. { ($relayhost,$relayhost_port) = ('*','*') }
  3952. $relayhost = "[$client_ip]" if $relayhost eq '*';
  3953. $relayhost_port = $conn->socket_port+1 if $relayhost_port eq '*';
  3954. $new_method = join(':', $via,$relayhost,$relayhost_port,@list[3..$#list]);
  3955. if ($new_method ne $method) {
  3956. do_log(3, "dynamic destination override: $method -> $new_method");
  3957. $method = $new_method;
  3958. }
  3959. }
  3960. $method;
  3961. }
  3962. sub mail_dispatch($$$$;$) {
  3963. my($conn) = shift;
  3964. my($msginfo,$initial_submission,$dsn_per_recip_capable,$filter) = @_;
  3965. my($via) = $msginfo->delivery_method;
  3966. if ($via =~ /^smtp:/i) {
  3967. mail_via_smtp(dynamic_destination($via,$conn), @_);
  3968. } elsif ($via =~ /^pipe:/i) {
  3969. mail_via_pipe($via, @_);
  3970. } elsif ($via =~ /^bsmtp:/i) {
  3971. mail_via_bsmtp($via, @_);
  3972. } elsif ($via =~ /^sql:/i) {
  3973. $Amavis::extra_code_sql_quar && $Amavis::sql_storage
  3974. or die "SQL quarantine code not enabled";
  3975. Amavis::Out::SQL::Quarantine::mail_via_sql(
  3976. $Amavis::sql_dataset_conn_storage, @_);
  3977. } elsif ($via =~ /^local:/i) {
  3978. # 'local:' is used by the quarantine code to relieve it
  3979. # of the need to know which delivery method needs to be used.
  3980. # Deliver first what is local (whatever does not contain '@')
  3981. mail_to_local_mailbox($via, $msginfo, $initial_submission,
  3982. sub { shift->recip_final_addr !~ /\@/ ? 1 : 0 });
  3983. if (grep { !$_->recip_done } @{$msginfo->per_recip_data}) {
  3984. my($nm) = c('notify_method'); # deliver the rest
  3985. if ($nm =~ /^smtp:/i) { mail_via_smtp(dynamic_destination($nm,$conn),@_)}
  3986. elsif ($nm =~ /^pipe:/i) { mail_via_pipe($nm, @_) }
  3987. elsif ($nm =~ /^bsmtp:/i) { mail_via_bsmtp($nm, @_) }
  3988. elsif ($nm =~ /^sql:/i) {
  3989. $Amavis::extra_code_sql_quar && $Amavis::sql_storage
  3990. or die "SQL quarantine code not enabled";
  3991. Amavis::Out::SQL::Quarantine::mail_via_sql(
  3992. $Amavis::sql_dataset_conn_storage, @_);
  3993. }
  3994. }
  3995. }
  3996. }
  3997. #sub Net::Cmd::debug_print {
  3998. # my($cmd,$out,$text) = @_;
  3999. # do_log(0, "*** ".$cmd->debug_text($out,$text)) if $out;
  4000. #}
  4001. # simple OO wrapper around Net::SMTP::datasend to provide a method 'print'
  4002. # and to buffer data, avoiding a bottleneck in Net::Cmd::datasend
  4003. #
  4004. sub new_smtp_data {
  4005. my($class, $handle) = @_;
  4006. bless { handle => $handle, buff => '' }, $class;
  4007. }
  4008. sub close { my($self) = shift; $self->flush }
  4009. sub print {
  4010. my($self) = shift; $self->{buff} .= join('',@_);
  4011. $self->flush if length($self->{buff}) >= 16384;
  4012. 1;
  4013. }
  4014. sub flush {
  4015. my($self) = shift;
  4016. if ($self->{buff} ne '') {
  4017. $self->{handle}->datasend($self->{buff})
  4018. or die "datasend timed out while sending buffered data\n";
  4019. $self->{buff} = '';
  4020. }
  4021. 1;
  4022. }
  4023. # Send mail using SMTP - do multiple transactions if necessary
  4024. # (e.g. due to '452 Too many recipients')
  4025. #
  4026. sub mail_via_smtp(@) {
  4027. my($via,$msginfo,$initial_submission,$dsn_per_recip_capable,$filter) = @_;
  4028. my($num_recips_undone) =
  4029. scalar(grep { !$_->recip_done && (!$filter || &$filter($_)) }
  4030. @{$msginfo->per_recip_data});
  4031. while ($num_recips_undone > 0) {
  4032. mail_via_smtp_single(@_); # send what we can in one transaction
  4033. my($num_recips_undone_after) =
  4034. scalar(grep { !$_->recip_done && (!$filter || &$filter($_)) }
  4035. @{$msginfo->per_recip_data});
  4036. if ($num_recips_undone_after >= $num_recips_undone) {
  4037. do_log(-2, "TROUBLE: Number of recipients ($num_recips_undone_after) "
  4038. . "not reduced in SMTP transaction, abandon the effort");
  4039. last;
  4040. }
  4041. if ($num_recips_undone_after > 0) {
  4042. do_log(1, sprintf("Sent to %s recipients via SMTP, %s still to go",
  4043. $num_recips_undone - $num_recips_undone_after,
  4044. $num_recips_undone_after));
  4045. }
  4046. $num_recips_undone = $num_recips_undone_after;
  4047. }
  4048. 1;
  4049. }
  4050. # Send mail using SMTP - single transaction
  4051. # (e.g. forwarding original mail or sending notification)
  4052. # May throw exception (die) if temporary failure (4xx) or other problem
  4053. #
  4054. sub mail_via_smtp_single(@) {
  4055. my($via,$msginfo,$initial_submission,$dsn_per_recip_capable,$filter) = @_;
  4056. my($which_section) = 'fwd_init';
  4057. snmp_count('OutMsgs');
  4058. local($1,$2,$3); # avoid Perl taint bug, still in 5.8.3
  4059. $via =~ /^smtp: (?: \[ ([^\]]*) \] | ([^:]*) ) : ([^:]*) /six
  4060. or die "Bad fwd method syntax: $via";
  4061. my($relayhost, $relayhost_port) = ($1.$2, $3);
  4062. my($mta_id) = sprintf("[%s]:%s", $relayhost, $relayhost_port);
  4063. my($btype) = $msginfo->body_type;
  4064. if (!defined $btype || uc($btype) eq '7BIT') { $btype = '' }
  4065. my($logmsg) = sprintf("%s via SMTP: %s", ($initial_submission?'SEND':'FWD'),
  4066. qquote_rfc2821_local($msginfo->sender) );
  4067. my(@per_recip_data) = grep { !$_->recip_done && (!$filter || &$filter($_)) }
  4068. @{$msginfo->per_recip_data};
  4069. if (!@per_recip_data) { do_log(5, "$logmsg, nothing to do"); return 1 }
  4070. ll(4) && do_log(4, "(about to connect to $mta_id) $logmsg -> " .
  4071. qquote_rfc2821_local(map {$_->recip_final_addr} @per_recip_data));
  4072. my($msg) = $msginfo->mail_text; # a file handle or a MIME::Entity object
  4073. my($smtp_handle, $smtp_response); my($smtp_code, $smtp_msg, $received_cnt);
  4074. my($any_valid_recips) = 0; my($any_tempfail_recips) = 0;
  4075. my($any_valid_recips_and_data_sent) = 0; my($in_datasend_mode) = 0;
  4076. if (defined($msg) && !$msg->isa('MIME::Entity')) {
  4077. # at this point, we have no idea what the user gave us...
  4078. # a globref? a FileHandle?
  4079. $msg = IO::Wrap::wraphandle($msg); # now we have an IO::Handle-like obj
  4080. $msg->seek(0,0) or die "Can't rewind mail file: $!";
  4081. }
  4082. # NOTE: Net::SMTP uses alarm to do its own timing.
  4083. # We need to restart our timer when Net::SMTP is done using it !!!
  4084. my($remaining_time) = alarm(0); # check how much time is left, stop timer
  4085. eval {
  4086. $which_section = 'fwd-connect';
  4087. # Timeout should be more than MTA normally takes to check DNS and RBL,
  4088. # which may take a minute or more in case of unreachable DNS server.
  4089. # Specifying shorter timeout will cause alarm to terminate the wait
  4090. # for SMTP status line prematurely, resulting in status code 000.
  4091. # rfc2821 (section 4.5.3.2) requires timeout to be at least 5 minutes
  4092. my($localaddr) = c('local_client_bind_address'); # IP assigned to socket
  4093. my($heloname) = c('localhost_name'); # host name used in HELO/EHLO
  4094. $! = 0; $@ = undef; # seems like Net::SMTP puts its error status in $@
  4095. $smtp_handle = Net::SMTP->new($relayhost, Port => $relayhost_port,
  4096. ($localaddr eq '' ? () : (LocalAddr => $localaddr)),
  4097. ($heloname eq '' ? () : (Hello => $heloname)),
  4098. ExactAddresses => 1,
  4099. Timeout => max(60, min(5 * 60, $remaining_time)), # for each operation
  4100. # Timeout => 0, # no timeouts, disable nonblocking mode on socket
  4101. # Debug => debug_oneshot(),
  4102. );
  4103. defined($smtp_handle) # don't change die text, it is referred to later
  4104. or die "Can't connect to $relayhost port $relayhost_port, $@ ($!)";
  4105. ll(5) && do_log(5,"Remote host presents itself as: ".$smtp_handle->domain);
  4106. section_time($which_section);
  4107. prolong_timer($which_section, $remaining_time); # restart timer
  4108. $remaining_time = undef;
  4109. $which_section = 'fwd-xforward';
  4110. if ($msginfo->client_addr ne '' && $smtp_handle->supports('XFORWARD')) {
  4111. my($cmd) = join(' ', 'XFORWARD', map
  4112. { my($n,$v) = @$_;
  4113. # may encode value as xtext/rfc3461 in future attributes:
  4114. # char between "!" (33) and "~" (126) inclusive, except "+" and "="
  4115. # $v =~ s/[^\041-\052\054-\074\076-\176]/sprintf("+%02X",ord($&))/eg;
  4116. # Wietse says not to xtext-encode these four attrs, just neuter them
  4117. $v =~ s/[^\041-\176]/?/g;
  4118. $v =~ s/[<>()\\";@]/?/g; # other chars that are special in headers
  4119. # postfix/smtpd/smtpd.c NEUTER_CHARACTERS (but ':' for IPv6)
  4120. $v = substr($v,0,255) if length($v) > 255; # see XFORWARD_README
  4121. $v eq '' ? () : ("$n=$v") }
  4122. ( ['ADDR', $msginfo->client_addr], ['NAME',$msginfo->client_name],
  4123. ['PROTO',$msginfo->client_proto],['HELO',$msginfo->client_helo] ));
  4124. do_log(5, "sending $cmd");
  4125. $smtp_handle->command($cmd);
  4126. $smtp_handle->response()==2 or die "sending $cmd\n";
  4127. section_time($which_section); prolong_timer($which_section);
  4128. }
  4129. $which_section = 'fwd-auth';
  4130. my($auth_user) = $msginfo->auth_user;
  4131. my($mechanisms) = $smtp_handle->supports('AUTH');
  4132. if (!c('auth_required_out')) {
  4133. do_log(3,"AUTH not needed, user='$auth_user', MTA offers '$mechanisms'");
  4134. } elsif ($mechanisms eq '') {
  4135. do_log(3,"INFO: MTA does not offer AUTH capability, user='$auth_user'");
  4136. } elsif (!defined $auth_user) {
  4137. do_log(0,"INFO: AUTH needed for submission but AUTH data not available");
  4138. } else {
  4139. do_log(3,"INFO: authenticating $auth_user, server supports AUTH $mechanisms");
  4140. my($sasl) = Authen::SASL->new(
  4141. 'callback' => { 'user' => $auth_user, 'authname' => $auth_user,
  4142. 'pass' => $msginfo->auth_pass });
  4143. $smtp_handle->auth($sasl) or die "sending AUTH, user=$auth_user\n";
  4144. section_time($which_section); prolong_timer($which_section);
  4145. }
  4146. $which_section = 'fwd-mail-from';
  4147. # how to pass the $msginfo->auth_submitter ???!!!
  4148. $smtp_handle->mail(qquote_rfc2821_local($msginfo->sender),
  4149. uc($btype) eq '8BITMIME' ? (Bits=>'8') : () )
  4150. or die "sending MAIL FROM\n";
  4151. section_time($which_section); prolong_timer($which_section);
  4152. $which_section = 'fwd-rcpt-to';
  4153. my($skipping_resp);
  4154. for my $r (@per_recip_data) { # send recipient addresses
  4155. if (defined $skipping_resp) {
  4156. $r->recip_smtp_response($skipping_resp); $r->recip_done(2);
  4157. next;
  4158. }
  4159. # send a RCPT TO command and get the response
  4160. my($raddr) = qquote_rfc2821_local($r->recip_final_addr);
  4161. $smtp_handle->recipient($raddr);
  4162. $smtp_code = $smtp_handle->code;
  4163. $smtp_msg = $smtp_handle->message;
  4164. chomp($smtp_msg);
  4165. my($rcpt_smtp_resp) = "$smtp_code $smtp_msg";
  4166. if ($smtp_code =~ /^2/) {
  4167. $any_valid_recips++;
  4168. do_log(3, "response to RCPT TO for $raddr: \"$rcpt_smtp_resp\"");
  4169. } else { # not ok
  4170. do_log(1, "response to RCPT TO for $raddr: \"$rcpt_smtp_resp\"");
  4171. if ($rcpt_smtp_resp =~ /^0/) {
  4172. # timeout, what to do, could cause duplicates
  4173. do_log(-1, "response to RCPT TO not yet available");
  4174. $rcpt_smtp_resp = "450 4.4.2 ($rcpt_smtp_resp - probably timed out)";
  4175. }
  4176. $r->recip_remote_mta($relayhost);
  4177. $r->recip_remote_mta_smtp_response($rcpt_smtp_resp);
  4178. if ($rcpt_smtp_resp =~ /^ (\d{3}) \s+ ([245] \. \d{1,3} \. \d{1,3})?
  4179. \s* (.*) \z/xs)
  4180. {
  4181. my($resp_code, $resp_enhcode, $resp_msg) = ($1, $2, $3);
  4182. if ($resp_enhcode eq '' && $resp_code =~ /^([245])/) {
  4183. my($c1) = $1;
  4184. $resp_enhcode = $resp_code eq '452' ? "$c1.5.3" : "$c1.1.0";
  4185. }
  4186. $rcpt_smtp_resp = sprintf("%s %s %s, id=%s, from MTA(%s): %s",
  4187. $resp_code, $resp_enhcode,
  4188. ($resp_code=~/^2/ ? 'Ok' : 'Failed'),
  4189. am_id(), $mta_id, $rcpt_smtp_resp);
  4190. }
  4191. if ($rcpt_smtp_resp =~ /^452/) { # too many recipients - see rfc2821
  4192. do_log(-1, sprintf('Only %d recips sent in one go: "%s"',
  4193. $any_valid_recips, $rcpt_smtp_resp));
  4194. $skipping_resp = $rcpt_smtp_resp;
  4195. } elsif ($rcpt_smtp_resp =~ /^4/) {
  4196. $any_tempfail_recips++;
  4197. $smtp_response = $rcpt_smtp_resp if !defined($smtp_response);
  4198. }
  4199. $r->recip_smtp_response($rcpt_smtp_resp); $r->recip_done(2);
  4200. $smtp_response = $rcpt_smtp_resp
  4201. if $rcpt_smtp_resp =~ /^5/ && $smtp_response !~ /^5/; # keep first 5x
  4202. }
  4203. }
  4204. section_time($which_section); prolong_timer($which_section);
  4205. $smtp_code = $smtp_msg = undef;
  4206. if (!$any_valid_recips) {
  4207. do_log(-1,"mail_via_smtp: DATA skipped, no valid recips, $any_tempfail_recips");
  4208. } elsif ($any_tempfail_recips && !$dsn_per_recip_capable) {
  4209. # we must not proceede if mail did not came in as LMTP,
  4210. # or we would generate mail duplicates on each delivery attempt
  4211. do_log(-1,"mail_via_smtp: DATA skipped, tempfailed recips: $any_tempfail_recips");
  4212. } else { # send the message contents (enter DATA phase)
  4213. $which_section = 'fwd-data';
  4214. $smtp_handle->data or die "sending DATA command\n";
  4215. $in_datasend_mode = 1;
  4216. my($smtp_resp) = $smtp_handle->code . " " . $smtp_handle->message;
  4217. chomp($smtp_resp);
  4218. do_log(4, "response to DATA: \"$smtp_resp\"");
  4219. # provide OO wrapper and buffering around Net::Cmd::datasend
  4220. my($smtp_data_fh) = Amavis::Out->new_smtp_data($smtp_handle);
  4221. my($hdr_edits) = $msginfo->header_edits;
  4222. $hdr_edits = Amavis::Out::EditHeader->new if !$hdr_edits;
  4223. $received_cnt = $hdr_edits->write_header($msg, $smtp_data_fh);
  4224. if ($received_cnt > 100) {
  4225. # loop detection required by rfc2821 6.2
  4226. # Do not modify the signal text, it gets matched elsewhere!
  4227. die "Too many hops: $received_cnt 'Received:' header lines\n";
  4228. }
  4229. if (!defined($msg)) {
  4230. # empty mail body
  4231. } elsif ($msg->isa('MIME::Entity')) {
  4232. warn "---------------------------------------------------------";
  4233. warn $msg->stringify;
  4234. $msg->print_body($smtp_data_fh);
  4235. } else {
  4236. my($nbytes,$buff);
  4237. # Using fixed-size reads instead of line-by-line approach
  4238. # makes feeding mail back to MTA (e.g. Postfix) more than
  4239. # twice as fast for larger mail.
  4240. ### # to reduce likelyhood of a qmail bare-LF bug (bare LF reported when
  4241. ### # CR and LF are separated by a TCP packet boundary) one may use this
  4242. ### # 'while' loop, reading line by line, instead of the normal one below
  4243. ### for (undef $!; defined($buff=$msg->getline); undef $!) {
  4244. ### $smtp_handle->datasend($buff)
  4245. ### or die "datasend timed out while sending body";
  4246. ### }
  4247. ### defined $buff || $!==0 or die "Error reading: $!";
  4248. # must flush buffering through $smtp_data_fh, as from now on
  4249. # we'll be calling Net::Cmd::datasend directly for speed
  4250. $smtp_data_fh->flush or die "Error flushing smtp_data_fh: $!";
  4251. while (($nbytes=$msg->read($buff,16384)) > 0) {
  4252. $smtp_handle->datasend($buff)
  4253. or die "datasend timed out while sending body";
  4254. }
  4255. defined $nbytes or die "Error reading: $!";
  4256. }
  4257. $smtp_data_fh->close or die "Error closing smtp_data_fh: $!";
  4258. $smtp_data_fh = undef;
  4259. section_time($which_section); prolong_timer($which_section);
  4260. $which_section = 'fwd-data-end';
  4261. # don't check status of dataend here, it may not yet be available
  4262. $smtp_handle->dataend;
  4263. $in_datasend_mode = 0; $any_valid_recips_and_data_sent = 1;
  4264. section_time($which_section); prolong_timer($which_section);
  4265. $which_section = 'fwd-rundown-1';
  4266. # figure out the final SMTP response
  4267. $smtp_code = $smtp_handle->code;
  4268. my(@msgs) = $smtp_handle->message;
  4269. # only the 'command()' resets messages list, so now we have both:
  4270. # 'End data with <CR><LF>.<CR><LF>' and 'Ok: queued as...' in @msgs
  4271. # and only the last SMTP response code in $smtp_handle->code
  4272. my($smtp_msg) = $msgs[$#msgs]; chomp($smtp_msg); # take the last one
  4273. $smtp_response = "$smtp_code $smtp_msg";
  4274. do_log(4, "response to data end: \"$smtp_response\"");
  4275. # credativ -jw
  4276. $smtp_response =~ /queued as (.*)$/;
  4277. do_log(0, "new postfix id: $1");
  4278. # credativ end
  4279. # replace success responses to RCPT TO commands with a final response
  4280. for my $r (@per_recip_data) {
  4281. next if $r->recip_done; # skip those that failed at RCPT TO
  4282. $r->recip_remote_mta($relayhost);
  4283. $r->recip_remote_mta_smtp_response($smtp_response);
  4284. }
  4285. }
  4286. };
  4287. my($err) = $@;
  4288. my($saved_section_name) = $which_section;
  4289. if ($err ne '') { chomp($err); $err = ' ' if $err eq '' } # careful chomp
  4290. prolong_timer($which_section, $remaining_time); # restart the timer
  4291. $which_section = 'fwd-rundown';
  4292. if ($err ne '') { # fetch info about failure
  4293. do_log(3, "mail_via_smtp: session failed: $err");
  4294. if (!defined($smtp_handle)) { $smtp_code = ''; $smtp_msg = '' }
  4295. else {
  4296. $smtp_code = $smtp_handle->code; $smtp_msg = $smtp_handle->message;
  4297. chomp($smtp_msg);
  4298. }
  4299. }
  4300. # terminate the SMTP session if still alive
  4301. if (!defined $smtp_handle) {
  4302. # nothing
  4303. } elsif ($in_datasend_mode) {
  4304. # We are aborting SMTP session. DATA send mode must NOT be normally
  4305. # terminated with a dataend (dot), otherwise recipient will receive
  4306. # a chopped-off mail (and possibly be receiving it over and over again
  4307. # during each MTA retry.
  4308. do_log(-1, "mail_via_smtp: NOTICE: aborting SMTP session, $err");
  4309. $smtp_handle->close; # abruptly terminate the SMTP session, ignoring status
  4310. } else {
  4311. $smtp_handle->timeout(15); # don't wait too long for response to a QUIT
  4312. $smtp_handle->quit; # send a QUIT regardless of success so far
  4313. if ($err eq '' && $smtp_handle->status != CMD_OK) {
  4314. do_log(-1,"WARN: sending SMTP QUIT command failed: "
  4315. . $smtp_handle->code . " " . $smtp_handle->message);
  4316. }
  4317. }
  4318. # prepare final smtp response and log abnormal events
  4319. if ($err eq '') { # no errors
  4320. if ($any_valid_recips_and_data_sent && $smtp_response !~ /^[245]/) {
  4321. $smtp_response =
  4322. sprintf("451 4.6.0 Bad SMTP code, id=%s, from MTA(%s): %s",
  4323. am_id(), $mta_id, $smtp_response);
  4324. } elsif ($smtp_response =~ /^((\d)\d{2})/) {
  4325. my($smtp_code,$smtp_status) = ($1,$2);
  4326. $smtp_response = sprintf("%s %d.6.0 %s, id=%s, from MTA(%s): %s",
  4327. $smtp_code, $smtp_status, ($smtp_status == 2 ? 'Ok' : 'Failed'),
  4328. am_id(), $mta_id, $smtp_response);
  4329. }
  4330. } elsif ($err eq "timed out" || $err =~ /: Timeout\z/) {
  4331. my($msg) = ($in_datasend_mode && $smtp_code =~ /^354/) ?
  4332. '' : ", $smtp_code $smtp_msg";
  4333. $smtp_response = sprintf("450 4.4.2 Timed out during %s%s, MTA(%s), id=%s",
  4334. $saved_section_name, $msg, $mta_id, am_id());
  4335. } elsif ($err =~ /^Can't connect/) {
  4336. $smtp_response = sprintf("450 4.4.1 %s, MTA(%s), id=%s",
  4337. $err, $mta_id, am_id());
  4338. } elsif ($err =~ /^Too many hops/) {
  4339. $smtp_response = sprintf("550 5.4.6 Rejected: %s, id=%s", $err, am_id());
  4340. } elsif ($smtp_code =~ /^5/) { # 5xx
  4341. $smtp_response = sprintf("%s 5.5.0 Rejected by MTA(%s): %s %s, id=%s",
  4342. ($smtp_code !~ /^5\d\d\z/ ? "550" : $smtp_code),
  4343. $mta_id, $smtp_code, $smtp_msg, am_id());
  4344. } elsif ($smtp_code =~ /^0/) { # 000
  4345. $smtp_response = sprintf("450 4.4.2 No response from MTA(%s) during %s (%s), id=%s",
  4346. $mta_id, $saved_section_name, $err, am_id());
  4347. } else {
  4348. $smtp_response = sprintf("%s 4.5.0 From MTA(%s) during %s (%s): %s %s, id=%s",
  4349. ($smtp_code !~ /^4\d\d\z/ ? "451" : $smtp_code),
  4350. $mta_id, $saved_section_name, $err,
  4351. $smtp_code, $smtp_msg, am_id());
  4352. }
  4353. do_log( ($smtp_response =~ /^2/ ? 1 : -1), $logmsg . " -> " .
  4354. qquote_rfc2821_local(map {$_->recip_final_addr} @per_recip_data) .
  4355. ", " . ($btype ne '' ? "BODY=$btype, " : '') . $smtp_response);
  4356. if (defined $smtp_response) {
  4357. for my $r (@per_recip_data) {
  4358. if (!$r->recip_done) { # mark it as done
  4359. $r->recip_smtp_response($smtp_response); $r->recip_done(2);
  4360. $r->recip_mbxname($r->recip_final_addr) if $smtp_response =~ /^2/;
  4361. } elsif ($any_valid_recips_and_data_sent
  4362. && $r->recip_smtp_response =~ /^452/) {
  4363. # 'undo' the RCPT TO '452 Too many recipients' situation,
  4364. # needs to be handled in more than one transaction
  4365. $r->recip_smtp_response(undef); $r->recip_done(undef);
  4366. }
  4367. }
  4368. }
  4369. if ( $smtp_response =~ /^2/) { snmp_count('OutMsgsDelivers') }
  4370. elsif ($smtp_response =~ /^4/) { snmp_count('OutAttemptFails') }
  4371. elsif ($smtp_response =~ /^5/) { snmp_count('OutMsgsRejects') }
  4372. section_time($which_section);
  4373. 1;
  4374. }
  4375. # Send mail using external mail submission program 'sendmail' (also available
  4376. # with Postfix and Exim) - used for forwarding original mail or sending notif.
  4377. # May throw exception (die) if temporary failure (4xx) or other problem
  4378. #
  4379. sub mail_via_pipe(@) {
  4380. my($via,$msginfo,$initial_submission,$dsn_per_recip_capable,$filter) = @_;
  4381. snmp_count('OutMsgs');
  4382. $via =~ /^pipe:(.*)\z/si or die "Bad fwd method syntax: $via";
  4383. my($pipe_args) = $1;
  4384. $pipe_args =~ s/^flags=\S*\s*//i; # flags are currently ignored, q implied
  4385. $pipe_args =~ s/^argv=//i;
  4386. my(@per_recip_data) = grep { !$_->recip_done && (!$filter || &$filter($_)) }
  4387. @{$msginfo->per_recip_data};
  4388. my($logmsg) = sprintf("%s via PIPE: %s", ($initial_submission?'SEND':'FWD'),
  4389. qquote_rfc2821_local($msginfo->sender));
  4390. if (!@per_recip_data) {
  4391. do_log(5, "$logmsg, nothing to do");
  4392. return 1;
  4393. }
  4394. do_log(1, $logmsg . " -> " .
  4395. qquote_rfc2821_local(map {$_->recip_final_addr} @per_recip_data));
  4396. my($msg) = $msginfo->mail_text; # a file handle or a MIME::Entity object
  4397. if (defined($msg) && !$msg->isa('MIME::Entity')) {
  4398. $msg = IO::Wrap::wraphandle($msg); # now we have an IO::Handle-like obj
  4399. $msg->seek(0,0) or die "Can't rewind mail file: $!";
  4400. }
  4401. my(@pipe_args) = split(' ', $pipe_args); my(@command) = shift @pipe_args;
  4402. for (@pipe_args) {
  4403. # The sendmail command line expects addresses quoted as per RFC 822.
  4404. # "funny user"@some.domain
  4405. # For compatibility with Sendmail, the Postfix sendmail command line
  4406. # also accepts address formats that are legal in RFC 822 mail headers:
  4407. # Funny Dude <"funny user"@some.domain>
  4408. # Although addresses passed as args to sendmail initial submission
  4409. # should not be <...> bracketed, for some reason original sendmail
  4410. # issues a warning on null reverse-path, but gladly accepty <>.
  4411. # As this is not strictly wrong, we comply to make it happy.
  4412. # NOTE: the -fsender is not allowed, -f and sender must be separate args!
  4413. if (/^\$\{sender\}\z/i) {
  4414. push(@command,
  4415. map { $_ eq '' ? '<>' : untaint(quote_rfc2821_local($_)) }
  4416. $msginfo->sender);
  4417. } elsif (/^\$\{recipient\}\z/i) {
  4418. push(@command,
  4419. map { $_ eq '' ? '<>' : untaint(quote_rfc2821_local($_)) }
  4420. map { $_->recip_final_addr } @per_recip_data);
  4421. } else {
  4422. push(@command, $_);
  4423. }
  4424. }
  4425. do_log(5, "mail_via_pipe running command: " . join(' ', @command));
  4426. local $SIG{CHLD} = 'DEFAULT';
  4427. local $SIG{PIPE} = 'IGNORE'; # write to broken pipe would throw a signal
  4428. my($mp,$pid) = run_command_consumer(undef,undef,@command);
  4429. binmode($mp) or die "Can't set pipe to binmode: $!"; # dflt since Perl 5.8.1
  4430. my($hdr_edits) = $msginfo->header_edits;
  4431. $hdr_edits = Amavis::Out::EditHeader->new if !$hdr_edits;
  4432. my($received_cnt) = $hdr_edits->write_header($msg, $mp);
  4433. if ($received_cnt > 100) { # loop detection required by rfc2821 6.2
  4434. # deal with it later, for now just skip the body
  4435. } elsif (!defined($msg)) {
  4436. # empty mail body
  4437. } elsif ($msg->isa('MIME::Entity')) {
  4438. $msg->print_body($mp);
  4439. } else {
  4440. my($nbytes,$buff);
  4441. while (($nbytes=$msg->read($buff,16384)) > 0)
  4442. { $mp->print($buff) or die "Submitting mail text failed: $!" }
  4443. defined $nbytes or die "Error reading: $!";
  4444. }
  4445. my($smtp_response);
  4446. if ($received_cnt > 100) { # loop detection required by rfc2821 6.2
  4447. do_log(-2, "Too many hops: $received_cnt 'Received:' header lines");
  4448. kill('TERM',$pid); # kill the process running mail submission program
  4449. $mp->close; # and ignore status
  4450. $smtp_response = "550 5.4.6 Rejected: " .
  4451. "Too many hops: $received_cnt 'Received:' header lines";
  4452. } else {
  4453. my($err); $mp->close or $err=$!; my($child_stat) = $?;
  4454. my($error_str) = exit_status_str($child_stat,$err);
  4455. my($status) = WEXITSTATUS($child_stat);
  4456. # sendmail program (Postfix variant) can return the following exit codes:
  4457. # EX_OK(0), EX_DATAERR, EX_SOFTWARE, EX_TEMPFAIL, EX_NOUSER, EX_UNAVAILABLE
  4458. if ($status == EX_OK) {
  4459. $smtp_response = "250 2.6.0 Ok"; # submitted to MTA
  4460. snmp_count('OutMsgsDelivers');
  4461. } elsif ($status == EX_TEMPFAIL) {
  4462. $smtp_response = "450 4.5.0 Temporary failure submitting message";
  4463. snmp_count('OutAttemptFails');
  4464. } elsif ($status == EX_NOUSER) {
  4465. $smtp_response = "550 5.1.1 Recipient unknown";
  4466. snmp_count('OutMsgsRejects');
  4467. } elsif ($status == EX_UNAVAILABLE) {
  4468. $smtp_response = "550 5.5.0 Mail submission service unavailable";
  4469. snmp_count('OutMsgsRejects');
  4470. } else {
  4471. $smtp_response = "451 4.5.0 Failed to submit a message: $error_str";
  4472. snmp_count('OutAttemptFails');
  4473. }
  4474. }
  4475. $smtp_response .= ", id=" . am_id();
  4476. for my $r (@per_recip_data) {
  4477. next if $r->recip_done;
  4478. $r->recip_smtp_response($smtp_response); $r->recip_done(2);
  4479. $r->recip_mbxname($r->recip_final_addr) if $smtp_response =~ /^2/;
  4480. }
  4481. section_time('fwd-pipe');
  4482. 1;
  4483. }
  4484. sub mail_via_bsmtp(@) {
  4485. my($via,$msginfo,$initial_submission,$dsn_per_recip_capable,$filter) = @_;
  4486. snmp_count('OutMsgs'); local($1);
  4487. $via =~ /^bsmtp:(.*)\z/si or die "Bad fwd method: $via";
  4488. my($bsmtp_file_final) = $1; my($mbxname);
  4489. my($s) = $msginfo->sender; # defanged sender name for use in filename
  4490. $s =~ tr/a-zA-Z0-9@._+-]/=/c;
  4491. $s = substr($s,0,100)."..." if length($s) > 100+3;
  4492. $s =~ s/\@/_at_/g; $s =~ s/^(\.{0,2})\z/_$1/g;
  4493. $bsmtp_file_final =~ s{%(.)}
  4494. { $1 eq 'b' ? $msginfo->body_digest
  4495. : $1 eq 'm' ? $msginfo->mail_id
  4496. : $1 eq 's' ? untaint($s)
  4497. : $1 eq 'i' ? iso8601_timestamp($msginfo->rx_time,1,'-')
  4498. : $1 eq 'n' ? am_id()
  4499. : $1 eq '%' ? '%' : '%'.$1 }egs;
  4500. # prepend directory if not specified
  4501. $bsmtp_file_final = $QUARANTINEDIR."/".$bsmtp_file_final
  4502. if $QUARANTINEDIR ne '' && $bsmtp_file_final !~ m{^/};
  4503. my($bsmtp_file_tmp) = $bsmtp_file_final . ".tmp";
  4504. my(@per_recip_data) = grep { !$_->recip_done && (!$filter || &$filter($_)) }
  4505. @{$msginfo->per_recip_data};
  4506. my($logmsg) = sprintf("%s via BSMTP: %s", ($initial_submission?'SEND':'FWD'),
  4507. qquote_rfc2821_local($msginfo->sender));
  4508. if (!@per_recip_data) { do_log(5, "$logmsg, nothing to do"); return 1 }
  4509. do_log(1, $logmsg . " -> " .
  4510. qquote_rfc2821_local(map {$_->recip_final_addr} @per_recip_data) .
  4511. ", file " . $bsmtp_file_final);
  4512. my($msg) = $msginfo->mail_text; # a scalar reference, or a file handle
  4513. if (defined($msg) && !$msg->isa('MIME::Entity')) {
  4514. $msg = IO::Wrap::wraphandle($msg); # now we have an IO::Handle-like obj
  4515. $msg->seek(0,0) or die "Can't rewind mail file: $!";
  4516. }
  4517. my($mp);
  4518. eval {
  4519. my($errn) = stat($bsmtp_file_tmp) ? 0 : 0+$!;
  4520. if ($errn == ENOENT) {} # good, no file, as expected
  4521. elsif (!$errn && -f _)
  4522. { die "File $bsmtp_file_tmp already exists, refuse to overwrite" }
  4523. else
  4524. { die "File $bsmtp_file_tmp exists??? Refuse to overwrite it, $!" }
  4525. $mp = IO::File->new;
  4526. $mp->open($bsmtp_file_tmp, O_CREAT|O_EXCL|O_WRONLY, 0640)
  4527. or die "Can't create BSMTP file $bsmtp_file_tmp: $!";
  4528. binmode($mp, ":bytes") or die "Can't set :bytes, $!" if $unicode_aware;
  4529. $mp->print("EHLO ", c('localhost_name'), $eol)
  4530. or die "print failed (EHLO): $!";
  4531. my($btype) = $msginfo->body_type;
  4532. if (!defined $btype || uc($btype) eq '7BIT') { $btype = '' }
  4533. $mp->printf("MAIL FROM:%s%s%s", # rfc1652: need "8bit Data"? (rfc2045)
  4534. qquote_rfc2821_local($msginfo->sender),
  4535. $btype ne '' ? ' BODY='.uc($btype) : '', $eol)
  4536. or die "print failed (MAIL FROM): $!";
  4537. for my $r (@per_recip_data) {
  4538. $mp->print("RCPT TO:", qquote_rfc2821_local($r->recip_final_addr), $eol)
  4539. or die "print failed (RCPT TO): $!";
  4540. }
  4541. $mp->print("DATA", $eol) or die "print failed (DATA): $!";
  4542. my($hdr_edits) = $msginfo->header_edits;
  4543. $hdr_edits = Amavis::Out::EditHeader->new if !$hdr_edits;
  4544. my($received_cnt) = $hdr_edits->write_header($msg,$mp);
  4545. if ($received_cnt > 100) { # loop detection required by rfc2821 6.2
  4546. die "Too many hops: $received_cnt 'Received:' header lines";
  4547. } elsif (!defined($msg)) { # empty mail body
  4548. } elsif ($msg->isa('MIME::Entity')) {
  4549. $msg->print_body($mp);
  4550. } else {
  4551. my($ln);
  4552. for (undef $!; defined($ln=$msg->getline); undef $!) {
  4553. $mp->print($ln=~/^\./ ?(".",$ln) :$ln) or die "print failed-data: $!";
  4554. }
  4555. defined $ln || $!==0 or die "Error reading: $!";
  4556. }
  4557. $mp->print(".", $eol) or die "print failed (final dot): $!";
  4558. # $mp->print("QUIT",$eol) or die "print failed (QUIT): $!";
  4559. $mp->close or die "Error closing BSMTP file $bsmtp_file_tmp: $!";
  4560. $mp = undef;
  4561. rename($bsmtp_file_tmp, $bsmtp_file_final)
  4562. or die "Can't rename BSMTP file to $bsmtp_file_final: $!";
  4563. $mbxname = $bsmtp_file_final;
  4564. };
  4565. my($err) = $@; my($smtp_response);
  4566. if ($err eq '') {
  4567. $smtp_response = "250 2.6.0 Ok, queued as BSMTP $bsmtp_file_final";
  4568. snmp_count('OutMsgsDelivers');
  4569. } else {
  4570. chomp($err);
  4571. unlink($bsmtp_file_tmp)
  4572. or do_log(-2,"Can't delete half-finished BSMTP file $bsmtp_file_tmp: $!");
  4573. $mp->close if defined $mp; # ignore status
  4574. if ($err =~ /too many hops/i) {
  4575. $smtp_response = "550 5.4.6 Rejected: $err";
  4576. snmp_count('OutMsgsRejects');
  4577. } else {
  4578. $smtp_response = "451 4.5.0 Writing $bsmtp_file_tmp failed: $err";
  4579. snmp_count('OutAttemptFails');
  4580. }
  4581. }
  4582. $smtp_response .= ", id=" . am_id();
  4583. for my $r (@per_recip_data) {
  4584. next if $r->recip_done;
  4585. $r->recip_smtp_response($smtp_response); $r->recip_done(2);
  4586. $r->recip_mbxname($mbxname) if $mbxname ne '' && $smtp_response =~ /^2/;
  4587. }
  4588. section_time('fwd-bsmtp');
  4589. 1;
  4590. }
  4591. 1;
  4592. #
  4593. package Amavis::UnmangleSender;
  4594. use strict;
  4595. use re 'taint';
  4596. BEGIN {
  4597. use Exporter ();
  4598. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  4599. $VERSION = '2.043';
  4600. @ISA = qw(Exporter);
  4601. @EXPORT_OK = qw(&best_try_originator_ip &best_try_originator
  4602. &first_received_from);
  4603. }
  4604. use subs @EXPORT_OK;
  4605. BEGIN {
  4606. import Amavis::Conf qw(:platform @viruses_that_fake_sender_maps);
  4607. import Amavis::Util qw(ll do_log);
  4608. import Amavis::rfc2821_2822_Tools qw(
  4609. split_address parse_received fish_out_ip_from_received);
  4610. import Amavis::Lookup qw(lookup);
  4611. import Amavis::Lookup::IP qw(lookup_ip_acl);
  4612. }
  4613. use Mail::Address;
  4614. # Returns the envelope sender address, or reconstructs it if there is
  4615. # a good reason to believe the envelope address has been changed or forged,
  4616. # as is common for some varieties of viruses. Returns best guess of the
  4617. # sender address, or undef if it can not be determined.
  4618. #
  4619. sub unmangle_sender($$$) {
  4620. my($sender) = shift; # rfc2821 envelope sender address
  4621. my($from) = shift; # rfc2822 'From:' header, may include comment
  4622. my($virusname_list) = shift; # list ref containing names of detected viruses
  4623. # based on ideas from Furio Ercolessi, Mike Atkinson, Mark Martinec
  4624. # my($localpart,$domain) = split_address($sender);
  4625. # # extract the RFC2822 'from' address, ignoring phrase and comment
  4626. # chomp($from);
  4627. # { local($1,$2,$3,$4); # avoid Perl 5.8.0 & 5.8.2 bug, $1 gets tainted !
  4628. # $from = (Mail::Address->parse($from))[0];
  4629. # }
  4630. # $from = $from->address if $from ne '';
  4631. # # NOTE: rfc2822 allows multiple addresses in the From field!
  4632. my($best_try_originator) = $sender;
  4633. if ($best_try_originator ne '') {
  4634. for my $vn (@$virusname_list) {
  4635. my($result,$matching_key) = lookup(0,$vn,@viruses_that_fake_sender_maps);
  4636. if ($result) {
  4637. do_log(2, "Virus $vn matches $matching_key, sender addr ignored");
  4638. $best_try_originator = undef; last;
  4639. }
  4640. }
  4641. }
  4642. $best_try_originator;
  4643. }
  4644. # Given a dotted-quad IPv4 address try reverse DNS resolve, and then
  4645. # forward DNS resolve. If they match, return domain name,
  4646. # otherwise return the IP address in brackets. (resolves IPv4 only)
  4647. #
  4648. sub ip_addr_to_name($) {
  4649. my($addr) = @_; # dotted-quad address string
  4650. local($1,$2,$3,$4); my($result);
  4651. if ($addr !~ /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\z/) {
  4652. $result = $addr; # not an IPv4 address
  4653. } else {
  4654. my($binaddr) = pack('C4', $1,$2,$3,$4); # to binary string
  4655. do_log(5, "ip_addr_to_name: DNS reverse-resolving: $addr");
  4656. my(@addr) = gethostbyaddr($binaddr,2); # IP -> name
  4657. $result = '['.$addr.']'; # IP address in brackets if nothing matches
  4658. if (@addr) {
  4659. my($name,$aliases,$addrtype,$length,@addrs) = @addr;
  4660. if ($name =~ /[^.]\.[a-zA-Z]+\z/s) {
  4661. do_log(5, "ip_addr_to_name: DNS forward-resolving: $name");
  4662. my(@raddr) = gethostbyname($name); # name -> IP
  4663. my($rname,$raliases,$raddrtype,$rlength,@raddrs) = @raddr;
  4664. for my $ra (@raddrs) {
  4665. if (lc($ra) eq lc($binaddr)) { $result = $name; last }
  4666. }
  4667. }
  4668. }
  4669. }
  4670. do_log(3, "ip_addr_to_name: returning: $result");
  4671. $result;
  4672. }
  4673. # Obtain and parse the first entry (chronologically) in the 'Received:' header
  4674. # path trace - to be used as the value of the macro %t in customized messages
  4675. #
  4676. sub first_received_from($) {
  4677. my($entity) = shift;
  4678. my($first_received);
  4679. if (defined($entity)) {
  4680. my($fields) = parse_received($entity->head->get('received', -1));
  4681. if (exists $fields->{'from'}) {
  4682. my($item, $v1, $v2, $v3, $comment) = @{$fields->{'from'}};
  4683. $first_received = join(' ', $item, $comment);
  4684. $first_received =~ s/^[ \t\n\r]+//s; # discard leading whitespace
  4685. $first_received =~ s/[ \t\n\r]+\z//s; # discard trailing whitespace
  4686. }
  4687. do_log(5, "first_received_from: $first_received");
  4688. }
  4689. $first_received;
  4690. }
  4691. # Try to extract sender's public IP address from the Received trace
  4692. #
  4693. use vars qw(@publicnetworks_maps);
  4694. sub best_try_originator_ip($) {
  4695. my($entity) = @_;
  4696. @publicnetworks_maps = (
  4697. Amavis::Lookup::Label->new('publicnetworks'),
  4698. Amavis::Lookup::IP->new(qw(
  4699. !0.0.0.0/8 !127.0.0.0/8 !172.16.0.0/12 !192.168.0.0/16 !10.0.0.0/8
  4700. !169.254.0.0/16 !192.0.2.0/24 !192.88.99.0/24 !224.0.0.0/4
  4701. [::FFFF:0:0]/96 ![::] ![::1] ![FF00::]/8 ![FE80::]/10 ![FEC0::]/10
  4702. [::]/0)) ) if !@publicnetworks_maps; # rfc3330, rfc3513
  4703. my($first_received_from_ip);
  4704. if (defined($entity)) {
  4705. my(@received) = reverse $entity->head->get_all('received');
  4706. $#received = 5 if $#received > 5; # first six, chronologically
  4707. for my $r (@received) {
  4708. $first_received_from_ip = fish_out_ip_from_received($r);
  4709. if ($first_received_from_ip ne '') {
  4710. my($is_public,$fullkey,$err) =
  4711. lookup_ip_acl($first_received_from_ip,@publicnetworks_maps);
  4712. last if (!defined($err) || $err eq '') && $is_public;
  4713. }
  4714. }
  4715. do_log(5, "best_try_originator_ip: $first_received_from_ip");
  4716. }
  4717. $first_received_from_ip;
  4718. }
  4719. # For the purpose of informing administrators try to obtain true sender
  4720. # address or at least its site, as most viruses and spam have a nasty habit
  4721. # of faking envelope sender address. Return a pair of addresses:
  4722. # - the first (if defined) appears valid and may be used for sender
  4723. # notifications;
  4724. # - the second should only be used in generating customizable notification
  4725. # messages (macro %o), NOT to be used as address for sending notifications,
  4726. # as it can contain invalid address (but can be more informative).
  4727. #
  4728. sub best_try_originator($$$) {
  4729. my($sender, $entity, $virusname_list) = @_;
  4730. my($from) = !defined($entity) ? '' : $entity->head->get('from',0);
  4731. my($originator) = unmangle_sender($sender,$from,$virusname_list);
  4732. return ($originator, $originator) if defined $originator;
  4733. my($first_received_from_ip) = best_try_originator_ip($entity);
  4734. $originator = '?@' . ip_addr_to_name($first_received_from_ip)
  4735. if $first_received_from_ip ne '';
  4736. (undef, $originator);
  4737. }
  4738. 1;
  4739. #
  4740. package Amavis::Unpackers::NewFilename;
  4741. use strict;
  4742. use re 'taint';
  4743. BEGIN {
  4744. use Exporter ();
  4745. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  4746. $VERSION = '2.043';
  4747. @ISA = qw(Exporter);
  4748. @EXPORT_OK = qw(&consumed_bytes);
  4749. }
  4750. BEGIN {
  4751. import Amavis::Conf qw(c cr ca
  4752. $MIN_EXPANSION_QUOTA $MIN_EXPANSION_FACTOR
  4753. $MAX_EXPANSION_QUOTA $MAX_EXPANSION_FACTOR);
  4754. import Amavis::Util qw(ll do_log min max);
  4755. }
  4756. use vars qw($avail_quota); # available bytes quota for unpacked mail
  4757. use vars qw($rem_quota); # remaining bytes quota for unpacked mail
  4758. sub new($;$$) { # create a file name generator object
  4759. my($class, $maxfiles,$mail_size) = @_;
  4760. # calculate and initialize quota
  4761. $avail_quota = $rem_quota = # quota in bytes
  4762. max($MIN_EXPANSION_QUOTA, $mail_size * $MIN_EXPANSION_FACTOR,
  4763. min($MAX_EXPANSION_QUOTA, $mail_size * $MAX_EXPANSION_FACTOR));
  4764. do_log(4,"Original mail size: $mail_size; quota set to: $avail_quota bytes");
  4765. # create object
  4766. bless {
  4767. num_of_issued_names => 0, first_issued_ind => 1, last_issued_ind => 0,
  4768. maxfiles => $maxfiles, # undef disables limit
  4769. objlist => [],
  4770. }, $class;
  4771. }
  4772. sub parts_list_reset($) { # clear a list of recently issued names
  4773. my($self) = shift;
  4774. $self->{num_of_issued_names} = 0;
  4775. $self->{first_issued_ind} = $self->{last_issued_ind} + 1;
  4776. $self->{objlist} = [];
  4777. }
  4778. sub parts_list($) { # returns a ref to a list of recently issued names
  4779. my($self) = shift;
  4780. $self->{objlist};
  4781. }
  4782. sub parts_list_add($$) { # add a parts object to the list of parts
  4783. my($self, $part) = @_;
  4784. push(@{$self->{objlist}}, $part);
  4785. }
  4786. sub generate_new_num($$) { # make-up a new number for a file and return it
  4787. my($self, $ignore_limit) = @_;
  4788. $ignore_limit = 0 if !defined($ignore_limit);
  4789. if (!$ignore_limit && defined($self->{maxfiles}) &&
  4790. $self->{num_of_issued_names} >= $self->{maxfiles}) {
  4791. # do not change the text in die without adjusting decompose_part()
  4792. die "Maximum number of files ($self->{maxfiles}) exceeded";
  4793. }
  4794. $self->{num_of_issued_names}++; $self->{last_issued_ind}++;
  4795. $self->{last_issued_ind};
  4796. }
  4797. sub consumed_bytes($$;$$) {
  4798. my($bytes, $bywhom, $tentatively, $exquota) = @_;
  4799. my($perc) = !$avail_quota ? '' : sprintf(", (%.0f%%)",
  4800. 100 * ($avail_quota - ($rem_quota - $bytes)) / $avail_quota);
  4801. ll(4) && do_log(4,"Charging $bytes bytes to remaining quota $rem_quota"
  4802. . " (out of $avail_quota$perc) - by $bywhom");
  4803. if ($bytes > $rem_quota && $rem_quota >= 0) {
  4804. # Do not modify the following signal text, it gets matched elsewhere!
  4805. my($msg) = "Exceeded storage quota $avail_quota bytes by $bywhom; ".
  4806. "last chunk $bytes bytes";
  4807. do_log(-1, $msg);
  4808. die "$msg\n" if !$exquota; # die, unless allowed to exceed quota
  4809. }
  4810. $rem_quota -= $bytes unless $tentatively;
  4811. $rem_quota; # return remaining quota
  4812. }
  4813. 1;
  4814. #
  4815. package Amavis::Unpackers::Part;
  4816. use strict;
  4817. use re 'taint';
  4818. BEGIN {
  4819. use Exporter ();
  4820. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  4821. $VERSION = '2.043';
  4822. @ISA = qw(Exporter);
  4823. }
  4824. BEGIN {
  4825. import Amavis::Util qw(ll do_log);
  4826. }
  4827. use vars qw($file_generator_object);
  4828. sub init($) { $file_generator_object = shift }
  4829. sub new($;$$$) { # create a part descriptor object
  4830. my($class, $dir_name,$parent,$ignore_limit) = @_;
  4831. my($self) = bless {}, $class;
  4832. if (!defined($dir_name) && !defined($parent)) {
  4833. # just make an empty object, presumably used as a new root
  4834. } else {
  4835. $self->number($file_generator_object->generate_new_num($ignore_limit));
  4836. $self->dir_name($dir_name) if defined $dir_name;
  4837. if (defined $parent) {
  4838. $self->parent($parent);
  4839. my($ch_ref) = $parent->children;
  4840. push(@$ch_ref,$self); $parent->children($ch_ref);
  4841. }
  4842. $file_generator_object->parts_list_add($self); # save it
  4843. ll(4) && do_log(4, "Issued a new " .
  4844. (defined $dir_name ? "file name" : "pseudo part") . ": " .
  4845. $self->base_name);
  4846. }
  4847. $self;
  4848. }
  4849. sub number
  4850. { my($self)=shift; !@_ ? $self->{number} : ($self->{number}=shift) };
  4851. sub dir_name
  4852. { my($self)=shift; !@_ ? $self->{dir_name} : ($self->{dir_name}=shift) };
  4853. sub parent
  4854. { my($self)=shift; !@_ ? $self->{parent} : ($self->{parent}=shift) };
  4855. sub children
  4856. { my($self)=shift; !@_ ? $self->{children}||[] : ($self->{children}=shift) };
  4857. sub mime_placement # part location within a MIME tree, e.g. "1/1/3"
  4858. { my($self)=shift; !@_ ? $self->{place} : ($self->{place}=shift) };
  4859. sub type_short # string or a ref to a list of strings
  4860. { my($self)=shift; !@_ ? $self->{ty_short} : ($self->{ty_short}=shift) };
  4861. sub type_long
  4862. { my($self)=shift; !@_ ? $self->{ty_long} : ($self->{ty_long}=shift) };
  4863. sub type_declared
  4864. { my($self)=shift; !@_ ? $self->{ty_decl} : ($self->{ty_decl}=shift) };
  4865. sub name_declared # string or a ref to a list of strings
  4866. { my($self)=shift; !@_ ? $self->{nm_decl} : ($self->{nm_decl}=shift) };
  4867. sub size
  4868. { my($self)=shift; !@_ ? $self->{size} : ($self->{size}=shift) };
  4869. sub exists
  4870. { my($self)=shift; !@_ ? $self->{exists} : ($self->{exists}=shift) };
  4871. sub attributes # listref of characters representing attributes
  4872. { my($self)=shift; !@_ ? $self->{attr} : ($self->{attr}=shift) };
  4873. sub attributes_add { # U=undecodable, C=crypted, D=directory,S=special,L=link
  4874. my($self)=shift; my($a) = $self->{attr} || [];
  4875. for my $arg (@_) { push(@$a,$arg) if $arg ne '' && !grep {$_ eq $arg} @$a }
  4876. $self->{attr} = $a;
  4877. };
  4878. sub base_name { my($self)=shift; sprintf("p%03d",$self->number) }
  4879. sub full_name {
  4880. my($self)=shift; my($d) = $self->dir_name;
  4881. !defined($d) ? undef : $d.'/'.$self->base_name;
  4882. }
  4883. # returns a ref to a list of part ancestors, starting with the root object,
  4884. # and including the part object itself
  4885. sub path {
  4886. my($self)=shift;
  4887. my(@path);
  4888. for (my($p)=$self; defined($p); $p=$p->parent) { unshift(@path,$p) }
  4889. \@path;
  4890. };
  4891. 1;
  4892. #
  4893. package Amavis::Unpackers::OurFiler;
  4894. use strict;
  4895. use re 'taint';
  4896. BEGIN {
  4897. use Exporter ();
  4898. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  4899. $VERSION = '2.043';
  4900. @ISA = qw(Exporter MIME::Parser::Filer); # subclass of MIME::Parser::Filer
  4901. }
  4902. # This package will be used by mime_decode().
  4903. #
  4904. # We don't want no heavy MIME::Parser machinery for file name extension
  4905. # guessing, decoding charsets in filenames (and listening to complaints
  4906. # about it), checking for evil filenames, checking for filename contention, ...
  4907. # (which can not be turned off completely by ignore_filename(1) !!!)
  4908. # Just enforce our file name! And while at it, collect generated filenames.
  4909. #
  4910. sub new($$$) {
  4911. my($class, $dir, $parent_obj) = @_;
  4912. $dir =~ s{/+\z}{}; # chop off trailing slashes from directory name
  4913. bless {parent => $parent_obj, directory => $dir}, $class;
  4914. }
  4915. # provide a generated file name
  4916. sub output_path($@) {
  4917. my($self, $head) = @_;
  4918. my($newpart_obj) =
  4919. Amavis::Unpackers::Part->new($self->{directory}, $self->{parent}, 1);
  4920. get_amavisd_part($head, $newpart_obj); # store object into head
  4921. $newpart_obj->full_name;
  4922. }
  4923. sub get_amavisd_part($;$) {
  4924. my($head) = shift;
  4925. !@_ ? $head->{amavisd_parts_obj} : ($head->{amavisd_parts_obj} = shift);
  4926. }
  4927. 1;
  4928. #
  4929. package Amavis::Unpackers::Validity;
  4930. use strict;
  4931. use re 'taint';
  4932. BEGIN {
  4933. use Exporter ();
  4934. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  4935. $VERSION = '2.043';
  4936. @ISA = qw(Exporter);
  4937. @EXPORT_OK = qw(&check_header_validity &check_for_banned_names);
  4938. }
  4939. BEGIN {
  4940. import Amavis::Util qw(ll do_log sanitize_str);
  4941. import Amavis::Conf qw(:platform %banned_rules c cr ca);
  4942. import Amavis::Lookup qw(lookup);
  4943. }
  4944. use subs @EXPORT_OK;
  4945. sub check_header_validity($$) {
  4946. my($conn, $msginfo) = @_;
  4947. my(@bad); my($curr_head);
  4948. for my $next_head (@{$msginfo->orig_header}, "\n") {
  4949. if ($next_head =~ /^[ \t]/) { $curr_head .= $next_head } # folded
  4950. else { # new header
  4951. if (!defined($curr_head)) { # no previous complete header
  4952. } else {
  4953. # obsolete rfc822 syntax allowed whitespace before colon
  4954. my($field_name, $field_body) =
  4955. $curr_head =~ /^([!-9;-\176]+)[ \t]*:(.*)\z/s
  4956. ? ($1, $2) : (undef, $curr_head);
  4957. my($msg1,$msg2);
  4958. if (!defined($field_name) && $curr_head=~/^()()(.*)\z/s) {
  4959. $msg1 = "Invalid header field syntax";
  4960. } elsif ($curr_head =~ /^(.*?)([\000\015])(.*)\z/s) {
  4961. $msg1 = "Improper use of control character";
  4962. } elsif ($curr_head =~ /^(.*?)([\200-\377])(.*)\z/s) {
  4963. $msg1 = "Non-encoded 8-bit data";
  4964. } elsif ($curr_head =~ /^(.*?)([^\000-\377])(.*)\z/s) {
  4965. $msg1 = "Non-encoded Unicode character"; # should not happen
  4966. } elsif ($curr_head =~ /^()()([ \t]+)$/m) {
  4967. $msg1 ="Improper folded header field made up entirely of whitespace";
  4968. }
  4969. if (defined $msg1) {
  4970. my($pre, $ch, $post) = ($1, $2, $3);
  4971. if (length($post) > 20) { $post = substr($post,0,15) . "..." }
  4972. if (length($pre)-length($field_name)-2 > 50-length($post)) {
  4973. $pre = "$field_name: ..."
  4974. . substr($pre, length($pre) - (45-length($post)));
  4975. }
  4976. $msg1 .= sprintf(" (char %02X hex)", ord($ch)) if length($ch)==1;
  4977. $msg1 .= " in message header '$field_name'" if $field_name ne '';
  4978. $msg2 = sanitize_str($pre); my($msg2_pre_l) = length($msg2);
  4979. $msg2 .= sanitize_str($ch . $post);
  4980. # push(@bad, "$msg1\n $msg2\n " . (' ' x $msg2_pre_l) . '^');
  4981. push(@bad, "$msg1: $msg2");
  4982. }
  4983. }
  4984. last if $next_head eq $eol; # end-of-header reached
  4985. last if @bad >= 100; # some sanity limit
  4986. $curr_head = $next_head;
  4987. }
  4988. }
  4989. ll(5) && do_log(5,"check_header: ".(!@bad ? "OK" : join(', ',@bad)));
  4990. @bad;
  4991. }
  4992. sub check_for_banned_names($$) {
  4993. my($msginfo,$parts_root) = @_;
  4994. do_log(3, "Checking for banned types and filenames");
  4995. my($bypmr) = ca('bypass_banned_checks_maps');
  4996. my($bfnmr) = ca('banned_filename_maps'); # two-level map: recip, partname
  4997. my(@recip_tables); # a list of records describing banned tables for recips
  4998. my($any_table_in_recip_tables) = 0; my($any_not_bypassed) = 0;
  4999. for my $r (@{$msginfo->per_recip_data}) {
  5000. my($recip) = $r->recip_addr;
  5001. my(@tables,@tables_m); # list of banned lookup tables for this recipient
  5002. if (!lookup(0,$recip,@$bypmr)) { # not bypassed
  5003. $any_not_bypassed = 1;
  5004. my($t_ref,$m_ref) = lookup(1,$recip,@$bfnmr);
  5005. if (defined $t_ref) {
  5006. for my $ti (0..$#$t_ref) { # collect all relevant tables for each recip
  5007. my($t) = $t_ref->[$ti];
  5008. # an entry may be a ref to a list of lookup tables, or a comma- or
  5009. # whitespace-separated list of table names (suitable for SQL),
  5010. # which are mapped to actual lookup tables through %banned_rules
  5011. if (!defined($t)) { # ignore
  5012. } elsif (ref($t) eq 'ARRAY') { # a list of actual lookup tables
  5013. push(@tables, @$t);
  5014. push(@tables_m, ($m_ref->[$ti]) x @$t);
  5015. } else { # a list of rules _names_, to be mapped via %banned_rules
  5016. my(@names); my(@rawnames) = grep { !/^[, ]*\z/ }
  5017. ($t =~ /\G (?: " (?: \\. | [^"\\] )* " | [^, ] )+ | [, ]+/gcsx);
  5018. # in principle the quoted strings could be used
  5019. # to construct lookup tables on-the-fly (not implemented)
  5020. for my $n (@rawnames) { # collect only valid names
  5021. if (!exists($banned_rules{$n})) {
  5022. do_log(2,"INFO: unknown banned table name $n, recip=$recip");
  5023. } elsif (!defined($banned_rules{$n})) { # ignore undef
  5024. } else { push(@names,$n) }
  5025. }
  5026. ll(3) && do_log(3,"collect banned table[$ti]: $recip, tables: ".
  5027. join(', ', map { $_.'=>'.$banned_rules{$_} } @names));
  5028. if (@names) { # any known and valid table names?
  5029. push(@tables, map { $banned_rules{$_} } @names);
  5030. push(@tables_m, ($m_ref->[$ti]) x @names);
  5031. }
  5032. }
  5033. }
  5034. }
  5035. }
  5036. push(@recip_tables, { r => $r, recip => $recip,
  5037. tables => \@tables, tables_m => \@tables_m } );
  5038. $any_table_in_recip_tables++ if @tables;
  5039. }
  5040. my($bnpre) = cr('banned_namepath_re');
  5041. if (!$any_not_bypassed) {
  5042. do_log(3,"skipping banned check: all recipients bypass banned checks");
  5043. } elsif (!$any_table_in_recip_tables && !(ref $bnpre && ref $$bnpre)) {
  5044. do_log(3,"skipping banned check: no applicable lookup tables");
  5045. } else {
  5046. do_log(4,"starting banned checks - traversing message structure tree");
  5047. my($part);
  5048. for (my(@unvisited)=($parts_root);
  5049. @unvisited and $part=shift(@unvisited);
  5050. push(@unvisited,@{$part->children}))
  5051. { # traverse decomposed parts tree breadth-first
  5052. my(@path) = @{$part->path};
  5053. next if @path <= 1;
  5054. shift(@path); # ignore place-holder root node
  5055. next if @{$part->children}; # ignore non-leaf nodes
  5056. my(@descr_trad); # a part path: list of predecessors of a message part
  5057. my(@descr); # same, but in form suitable for check on banned_namepath_re
  5058. for my $p (@path) {
  5059. my(@k,$n);
  5060. $n = $p->base_name;
  5061. if ($n ne '') { $n=~s/[\t\n]/ /g; push(@k,"P=$n") }
  5062. $n = $p->mime_placement;
  5063. if ($n ne '') { $n=~s/[\t\n]/ /g; push(@k,"L=$n") }
  5064. $n = $p->type_declared;
  5065. $n = [$n] if !ref($n);
  5066. for (@$n) {if ($_ ne ''){my($m)=$_; $m=~s/[\t\n]/ /g; push(@k,"M=$m")}}
  5067. $n = $p->type_short;
  5068. $n = [$n] if !ref($n);
  5069. for (@$n) {if (defined($_) && $_ ne '')
  5070. {my($m)=$_; $m=~s/[\t\n]/ /g; push(@k,"T=$m")} }
  5071. $n = $p->name_declared;
  5072. $n = [$n] if !ref($n);
  5073. for (@$n) {if (defined($_) && $_ ne '')
  5074. {my($m)=$_; $m=~s/[\t\n]/ /g; push(@k,"N=$m")} }
  5075. $n = $p->attributes;
  5076. $n = [$n] if !ref($n);
  5077. for (@$n) {if (defined($_) && $_ ne '')
  5078. {my($m)=$_; $m=~s/[\t\n]/ /g; push(@k,"A=$m")} }
  5079. push(@descr, join("\t",@k));
  5080. push(@descr_trad, [map { local($1,$2);
  5081. /^([a-zA-Z0-9])=(.*)\z/s; my($key_what,$key_val) = ($1,$2);
  5082. $key_what eq 'M' || $key_what eq 'N' ? $key_val
  5083. : $key_what eq 'T' ? ('.'.$key_val) # prepend a dot (compatibility)
  5084. : $key_what eq 'A' && $key_val eq 'U' ? 'UNDECIPHERABLE' : ()} @k]);
  5085. }
  5086. # we have obtained a description of a part as a list of its predecessors
  5087. # in a message structure including the part itself at the end of the list
  5088. my($key_val_str) = join(' | ',@descr); $key_val_str =~ s/\t/,/g;
  5089. my($key_val_trad_str) = join(' | ', map {join(',',@$_)} @descr_trad);
  5090. # evaluate current mail component path against each recipients' tables
  5091. ll(4) && do_log(4, sprintf("check_for_banned (%s) %s",
  5092. join(',', map {$_->base_name} @path), $key_val_trad_str));
  5093. my($result,$matchingkey); my($t_ref_old);
  5094. for my $e (@recip_tables) { # for each recipient and his tables
  5095. my($found,$recip,$t_ref) = @$e{'found','recip','tables'};
  5096. if (!$e->{result} && $t_ref && @$t_ref) {
  5097. my($same_as_prev) = $t_ref_old && @$t_ref_old==@$t_ref &&
  5098. !(grep { $t_ref_old->[$_] ne $t_ref->[$_] }
  5099. (0..$#$t_ref)) ? 1 : 0;
  5100. if ($same_as_prev) {
  5101. do_log(4,"skip banned check for $recip, ".
  5102. "same tables as previous, result => $result");
  5103. } else {
  5104. do_log(5,"doing banned check for $recip on ".$key_val_trad_str);
  5105. ($result,$matchingkey) =
  5106. lookup(0, [map {@$_} @descr_trad], # check all attribs in one go
  5107. Amavis::Lookup::Label->new("check_bann:$recip"),
  5108. map { ref($_) eq 'ARRAY' ? @$_ : $_ } @$t_ref);
  5109. $t_ref_old = $t_ref;
  5110. }
  5111. @$e{'found','result','matchk','part_descr'} =
  5112. (1,$result,$matchingkey,$key_val_trad_str) if defined $result;
  5113. }
  5114. }
  5115. if (ref $bnpre && ref $$bnpre &&
  5116. grep {!$_->{result}} @recip_tables) { # any non-true remains
  5117. # try new style: banned_namepath_re; it is global, not per-recipient
  5118. my($result,$matchingkey) = lookup(0, join("\n",@descr),
  5119. Amavis::Lookup::Label->new('banned_namepath_re'), $bnpre);
  5120. if (defined $result) {
  5121. for my $e (@recip_tables) {
  5122. @$e{'found','result','matchk','part_descr'} =
  5123. (1,$result,$matchingkey,$key_val_str) if !$e->{found};
  5124. }
  5125. }
  5126. }
  5127. my(%esc) = (r => "\r", n => "\n", f => "\f", b => "\b",
  5128. e => "\e", a => "\a", t => "\t"); # for pretty-printing
  5129. my($ll) = (grep {$_->{result}} @recip_tables) ? 1 : 3; # log level
  5130. for my $e (@recip_tables) { # log and store results
  5131. my($r,$recip,$result,$matchingkey,$part_descr) =
  5132. @$e{'r','recip','result','matchk','part_descr'};
  5133. if (ll($ll)) { # only bother with logging when needed
  5134. my($mk) = defined $matchingkey ? $matchingkey : ''; # pretty-print
  5135. $mk =~ s{ \\(.) }{ exists($esc{$1}) ? $esc{$1} : '\\'.$1 }egsx;
  5136. do_log($result?1:3, sprintf('p.path%s %s: "%s"%s',
  5137. !$result?'':" BANNED:$result", $recip, $key_val_str,
  5138. !defined $result ? '' : ", matching_key=\"$mk\""));
  5139. }
  5140. my($a);
  5141. if ($result) { # the part being tested is banned for this recipient
  5142. $a = $r->banned_parts; $a = [] if !defined($a);
  5143. push(@$a,$part_descr); $r->banned_parts($a);
  5144. $a = $r->banned_keys; $a = [] if !defined($a);
  5145. push(@$a,$matchingkey); $r->banned_keys($a);
  5146. $a = $r->banned_rhs; $a = [] if !defined($a);
  5147. push(@$a,$result); $r->banned_rhs($a);
  5148. }
  5149. }
  5150. last if !grep {!$_->{result}} @recip_tables; # stop if all recips true
  5151. } # endfor: message tree traversal
  5152. } # endif: doing parts checking
  5153. }
  5154. 1;
  5155. #
  5156. package Amavis::Unpackers::MIME;
  5157. use strict;
  5158. use re 'taint';
  5159. BEGIN {
  5160. use Exporter ();
  5161. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  5162. $VERSION = '2.043';
  5163. @ISA = qw(Exporter);
  5164. @EXPORT_OK = qw(&mime_decode);
  5165. }
  5166. use Errno qw(ENOENT EACCES);
  5167. use IO::File qw(O_CREAT O_EXCL O_WRONLY);
  5168. use MIME::Parser;
  5169. use MIME::Words;
  5170. BEGIN {
  5171. import Amavis::Conf qw(:platform c cr ca);
  5172. import Amavis::Timing qw(section_time);
  5173. import Amavis::Util qw(snmp_count ll do_log);
  5174. import Amavis::Unpackers::NewFilename qw(consumed_bytes);
  5175. }
  5176. use subs @EXPORT_OK;
  5177. # save MIME preamble and epilogue (if nontrivial) as extra (pseudo)parts
  5178. sub mime_decode_pre_epi($$$$$) {
  5179. my($pe_name, $pe_lines, $tempdir, $parent_obj, $placement) = @_;
  5180. if (defined $pe_lines && @$pe_lines) {
  5181. do_log(5, "mime_decode_$pe_name: " . scalar(@$pe_lines) . " lines");
  5182. if (@$pe_lines > 5 || "@$pe_lines" !~ m{^[a-zA-Z0-9/\@:;,. \t\n_-]*\z}s) {
  5183. my($newpart_obj) =
  5184. Amavis::Unpackers::Part->new("$tempdir/parts",$parent_obj,1);
  5185. $newpart_obj->mime_placement($placement);
  5186. $newpart_obj->name_declared($pe_name);
  5187. my($newpart) = $newpart_obj->full_name;
  5188. my($outpart) = IO::File->new;
  5189. $outpart->open($newpart, O_CREAT|O_EXCL|O_WRONLY, 0640)
  5190. or die "Can't create $pe_name file $newpart: $!";
  5191. binmode($outpart, ":bytes") or die "Can't cancel :utf8 mode: $!"
  5192. if $unicode_aware;
  5193. my($len);
  5194. for (@$pe_lines) {
  5195. $outpart->print($_) or die "Can't write $pe_name to $newpart: $!";
  5196. $len += length($_);
  5197. }
  5198. $outpart->close or die "Error closing $pe_name $newpart: $!";
  5199. $newpart_obj->size($len);
  5200. consumed_bytes($len, "mime_decode_$pe_name", 0, 1);
  5201. }
  5202. }
  5203. }
  5204. # traverse MIME::Entity object depth-first,
  5205. # extracting preambles and epilogues as extra (pseudo)parts, and
  5206. # filling-in additional information into Amavis::Unpackers::Part objects
  5207. sub mime_traverse($$$$$); # prototype
  5208. sub mime_traverse($$$$$) {
  5209. my($entity, $tempdir, $parent_obj, $depth, $placement) = @_;
  5210. mime_decode_pre_epi('preamble', $entity->preamble,
  5211. $tempdir, $parent_obj, $placement);
  5212. my($mt, $et) = ($entity->mime_type, $entity->effective_type);
  5213. my($part); my($head) = $entity->head; my($body) = $entity->bodyhandle;
  5214. if (!defined($body)) { # a MIME container only contains parts, no bodypart
  5215. # create pseudo-part objects for MIME containers (e.g. multipart/* )
  5216. $part = Amavis::Unpackers::Part->new(undef,$parent_obj,1);
  5217. # $part->type_short('no-file');
  5218. do_log(2, $part->base_name." $placement Content-Type: $mt");
  5219. } else { # does have a body part (i.e. not a MIME container)
  5220. my($fn) = $body->path; my($size);
  5221. if (!defined($fn)) { $size = length($body->as_string) }
  5222. else {
  5223. my($msg); my($errn) = lstat($fn) ? 0 : 0+$!;
  5224. if ($errn == ENOENT) { $msg = "does not exist" }
  5225. elsif ($errn) { $msg = "is inaccessible: $!" }
  5226. elsif (!-r _) { $msg = "is not readable" }
  5227. elsif (!-f _) { $msg = "is not a regular file" }
  5228. else {
  5229. $size = -s _;
  5230. do_log(4,"mime_traverse: file $fn is empty") if !$size;
  5231. }
  5232. do_log(-1,"WARN: mime_traverse: file $fn $msg") if defined $msg;
  5233. }
  5234. consumed_bytes($size, 'mime_decode', 0, 1);
  5235. # retrieve Amavis::Unpackers::Part object (if any), stashed into head obj
  5236. $part = Amavis::Unpackers::OurFiler::get_amavisd_part($head);
  5237. if (defined $part) {
  5238. $part->size($size);
  5239. if ($size==0) { $part->type_short('empty'); $part->type_long('empty') }
  5240. ll(2) && do_log(2, $part->base_name." $placement Content-Type: $mt" .
  5241. ", size: $size B, name: ".$entity->head->recommended_filename);
  5242. my($old_parent_obj) = $part->parent;
  5243. if ($parent_obj ne $old_parent_obj) { # reparent if necessary
  5244. ll(5) && do_log(5,sprintf("reparenting %s from %s to %s",
  5245. $part->base_name,
  5246. $old_parent_obj->base_name, $parent_obj->base_name));
  5247. my($ch_ref) = $old_parent_obj->children;
  5248. $old_parent_obj->children([grep {$_ ne $part} @$ch_ref]);
  5249. $ch_ref = $parent_obj->children;
  5250. push(@$ch_ref,$part); $parent_obj->children($ch_ref);
  5251. $part->parent($parent_obj);
  5252. }
  5253. }
  5254. }
  5255. if (defined $part) {
  5256. $part->mime_placement($placement);
  5257. $part->type_declared($mt eq $et ? $mt : [$mt, $et]);
  5258. my(@rn); # recommended file names, both raw and RFC 2047 decoded
  5259. my($val, $val_decoded);
  5260. $val = $head->mime_attr('content-disposition.filename');
  5261. if ($val ne '') {
  5262. push(@rn, $val);
  5263. $val_decoded = MIME::Words::decode_mimewords($val);
  5264. push(@rn, $val_decoded) if $val_decoded ne $val;
  5265. }
  5266. $val = $head->mime_attr('content-type.name');
  5267. if (defined($val) && $val ne '') {
  5268. $val_decoded = MIME::Words::decode_mimewords($val);
  5269. push(@rn, $val_decoded) if !grep { $_ eq $val_decoded } @rn;
  5270. push(@rn, $val) if !grep { $_ eq $val } @rn;
  5271. }
  5272. $part->name_declared(@rn==1 ? $rn[0] : \@rn) if @rn;
  5273. }
  5274. mime_decode_pre_epi('epilogue', $entity->epilogue,
  5275. $tempdir, $parent_obj, $placement);
  5276. my($item_num) = 0;
  5277. for my $e ($entity->parts) { # recursive descent
  5278. $item_num++;
  5279. mime_traverse($e,$tempdir,$part,$depth+1,"$placement/$item_num");
  5280. }
  5281. }
  5282. # Break up mime parts, return MIME::Entity object
  5283. sub mime_decode($$$) {
  5284. my($fileh, $tempdir, $parent_obj) = @_;
  5285. # $fileh may be an open file handle, or a file name
  5286. my($parser) = MIME::Parser->new;
  5287. $parser->filer(Amavis::Unpackers::OurFiler->new("$tempdir/parts",
  5288. $parent_obj));
  5289. $parser->ignore_errors(1); # also is the default
  5290. # $parser->extract_nested_messages(0);
  5291. $parser->extract_nested_messages("NEST"); # parse embedded message/rfc822
  5292. $parser->extract_uuencode(1); # to enable or not to enable ???
  5293. my($entity);
  5294. snmp_count('OpsDecByMimeParser');
  5295. if (ref($fileh)) { # assume open file handle
  5296. do_log(4, "Extracting mime components");
  5297. $fileh->seek(0,0) or die "Can't rewind mail file: $!";
  5298. local($1,$2,$3,$4); # avoid Perl 5.8.0 & 5.8.2 bug, $1 gets tainted !
  5299. $entity = $parser->parse($fileh);
  5300. } else { # assume $fileh is a file name
  5301. do_log(4, "Extracting mime components from $fileh");
  5302. local($1,$2,$3,$4); # avoid Perl 5.8.0 & 5.8.2 bug, $1 gets tainted !
  5303. $entity = $parser->parse_open("$tempdir/parts/$fileh");
  5304. }
  5305. # my($mime_err) = $parser->last_error; # deprecated
  5306. my($mime_err) = $parser->results->errors;
  5307. if (defined $mime_err) {
  5308. $mime_err=~s/\s+\z//; $mime_err=~s/[ \t\r]*\n+/; /g; $mime_err=~s/\s+/ /g;
  5309. $mime_err = substr($mime_err,0,250) . '...' if length($mime_err) > 250;
  5310. do_log(1, "WARN: MIME::Parser $mime_err") if $mime_err ne '';
  5311. }
  5312. mime_traverse($entity, $tempdir, $parent_obj, 0, '1');
  5313. section_time('mime_decode');
  5314. ($entity, $mime_err);
  5315. }
  5316. 1;
  5317. #
  5318. package Amavis::Notify;
  5319. use strict;
  5320. use re 'taint';
  5321. BEGIN {
  5322. use Exporter ();
  5323. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  5324. $VERSION = '2.043';
  5325. @ISA = qw(Exporter);
  5326. @EXPORT_OK = qw(&delivery_status_notification &delivery_short_report
  5327. &string_to_mime_entity &defanged_mime_entity
  5328. &msg_from_quarantine);
  5329. }
  5330. BEGIN {
  5331. import Amavis::Util qw(ll do_log am_id safe_encode q_encode);
  5332. import Amavis::Timing qw(section_time);
  5333. import Amavis::Conf qw(:platform $myhostname c cr ca);
  5334. import Amavis::Lookup qw(lookup);
  5335. import Amavis::Expand qw(expand);
  5336. import Amavis::rfc2821_2822_Tools;
  5337. }
  5338. use MIME::Entity;
  5339. # use Encode; # Perl 5.8 UTF-8 support
  5340. use subs @EXPORT_OK;
  5341. # Convert mail (that was obtained by macro-expanding notification templates)
  5342. # into proper MIME::Entity object. Some ad-hoc solutions are used
  5343. # for compatibility with previous version.
  5344. #
  5345. sub string_to_mime_entity($) {
  5346. my($mail_as_string_ref) = @_;
  5347. local($1,$2,$3); my($entity); my($m_hdr,$m_body);
  5348. ($m_hdr, $m_body) = ($1, $3)
  5349. if $$mail_as_string_ref =~ /^(.*?\r?\n)(\r?\n|\z)(.*)\z/s;
  5350. $m_body = safe_encode(c('bdy_encoding'), $m_body);
  5351. # make sure _our_ source line number is reported in case of failure
  5352. my($nxmh) = c('notify_xmailer_header');
  5353. eval {$entity = MIME::Entity->build(
  5354. Type => 'text/plain', Encoding => '-SUGGEST', Charset=> c('bdy_encoding'),
  5355. (defined $nxmh && $nxmh eq '' ? () # leave the MIME::Entity default
  5356. : ('X-Mailer' => $nxmh) ), # X-Mailer hdr or undef
  5357. Data => $m_body); 1} or do {chomp($@); die $@};
  5358. my($head) = $entity->head;
  5359. # insert header fields from template into MIME::Head entity
  5360. $m_hdr =~ s/\r?\n([ \t])/$1/g; # unfold template header
  5361. for my $hdr_line (split(/\r?\n/, $m_hdr)) {
  5362. if ($hdr_line =~ /^([^:]*):\s*(.*)\z/s) {
  5363. my($fhead, $fbody) = ($1, $2);
  5364. # encode according to RFC 2047 if necessary
  5365. $fhead = safe_encode('ascii', $fhead);
  5366. if ($fhead =~ /^(X-.*|Subject|Comments)\z/si &&
  5367. $fbody =~ /[^\011\012\040-\176]/) # nonprint. except TAB and LF?
  5368. { # encode according to RFC 2047
  5369. # TODO: shouldn't we unfold first?!
  5370. my($fbody_octets);
  5371. if (!$unicode_aware) { $fbody_octets = $fbody }
  5372. else {
  5373. $fbody_octets = safe_encode(c('hdr_encoding'), $fbody);
  5374. do_log(5, "string_to_mime_entity UTF-8 body: $fbody");
  5375. do_log(5, "string_to_mime_entity body octets: $fbody_octets");
  5376. }
  5377. my($qb) = c('hdr_encoding_qb');
  5378. if (uc($qb) eq 'Q') {
  5379. $fbody = q_encode($fbody_octets, $qb, c('hdr_encoding'));
  5380. } else {
  5381. $fbody = MIME::Words::encode_mimeword($fbody_octets,
  5382. $qb, c('hdr_encoding'));
  5383. }
  5384. } else { # supposed to be in plain ASCII, let's make sure it is
  5385. $fbody = safe_encode('ascii', $fbody);
  5386. }
  5387. do_log(5, sprintf("string_to_mime_entity %s: %s", $fhead, $fbody));
  5388. # make sure _our_ source line number is reported in case of failure
  5389. if (!eval { $head->replace($fhead, $fbody); 1 }) {
  5390. chomp($@);
  5391. die sprintf("%s header field '%s: %s'",
  5392. ($@ eq '' ? "invalid" : "$@, "), $fhead, $fbody);
  5393. }
  5394. }
  5395. }
  5396. $entity; # return the built MIME::Entity
  5397. }
  5398. # Generate delivery status notification according to
  5399. # rfc1892 (now rfc3462) and rfc1894 (now rfc3464).
  5400. # Return dsn message object if dsn is needed, or undef otherwise.
  5401. #
  5402. sub delivery_status_notification($$$$$) {
  5403. my($conn,$msginfo,$report_success_dsn_also,$builtins_ref,$template_ref) = @_;
  5404. my($dsn_time) = time; # time of dsn creation - now
  5405. my($notification);
  5406. if ($msginfo->sender eq '') { # must not respond to null reverse path
  5407. do_log(4, "Not sending DSN to empty return path");
  5408. } else {
  5409. my($from_mta, $client_ip) = ($conn->smtp_helo, $conn->client_ip);
  5410. my($msg) = ''; # constructed dsn text according to rfc3464
  5411. $msg .= "Reporting-MTA: dns; $myhostname\n";
  5412. $msg .= "Received-From-MTA: smtp; $from_mta ([$client_ip])\n"
  5413. if $from_mta ne '';
  5414. $msg .= "Arrival-Date: " . rfc2822_timestamp($msginfo->rx_time) . "\n";
  5415. my($any); # any recipients with failed delivery?
  5416. for my $r (@{$msginfo->per_recip_data}) {
  5417. my($remote_mta) = $r->recip_remote_mta;
  5418. my($smtp_resp) = $r->recip_smtp_response;
  5419. if (!$r->recip_done) {
  5420. if ($msginfo->delivery_method eq '') { # e.g. milter
  5421. # as far as we are concerned all is ok, delivery will be performed
  5422. # by a helper program or MTA
  5423. $smtp_resp = "250 2.5.0 Ok, continue delivery";
  5424. } else {
  5425. do_log(-2,"TROUBLE: recipient not done: <"
  5426. . $r->recip_addr . "> " . $smtp_resp);
  5427. }
  5428. }
  5429. my($smtp_resp_code, $smtp_resp_enhcode, $smtp_resp_msg);
  5430. if ($smtp_resp =~ /^ (\d{3}) \s+ ([245] \. \d{1,3} \. \d{1,3})?
  5431. \s* (.*) \z/xs) {
  5432. ($smtp_resp_code, $smtp_resp_enhcode, $smtp_resp_msg) = ($1,$2,$3);
  5433. } else {
  5434. $smtp_resp_msg = $smtp_resp;
  5435. }
  5436. my($smtp_resp_class) = $smtp_resp_code =~ /^(\d)/ ? $1 : '0';
  5437. if ($smtp_resp_enhcode eq '' && $smtp_resp_class =~ /^([245])\z/) {
  5438. $smtp_resp_enhcode = "$1.0.0";
  5439. }
  5440. # skip success notifications
  5441. next unless $smtp_resp_class ne '2' || $report_success_dsn_also;
  5442. $any++;
  5443. $msg .= "\n"; # empty line between groups of per-recipient fields
  5444. if ($remote_mta ne '' && $r->recip_final_addr ne $r->recip_addr) {
  5445. $msg .= "X-NextToLast-Final-Recipient: rfc822; "
  5446. . quote_rfc2821_local($r->recip_addr) . "\n";
  5447. $msg .= "Final-Recipient: rfc822; "
  5448. . quote_rfc2821_local($r->recip_final_addr) . "\n";
  5449. } else {
  5450. $msg .= "Final-Recipient: rfc822; "
  5451. . quote_rfc2821_local($r->recip_addr) . "\n";
  5452. }
  5453. $msg .= "Action: ".($smtp_resp_class eq '2' ? 'delivered':'failed')."\n";
  5454. $msg .= "Status: $smtp_resp_enhcode\n";
  5455. my($rem_smtp_resp) = $r->recip_remote_mta_smtp_response;
  5456. if ($remote_mta eq '' || $rem_smtp_resp eq '') {
  5457. $msg .= "Diagnostic-Code: smtp; $smtp_resp\n";
  5458. } else {
  5459. $msg .= "Remote-MTA: dns; $remote_mta\n";
  5460. $msg .= "Diagnostic-Code: smtp; $rem_smtp_resp\n";
  5461. }
  5462. $msg .= "Last-Attempt-Date: " . rfc2822_timestamp($dsn_time) . "\n";
  5463. }
  5464. return $notification if !$any; # don't bother, we won't be sending DSN
  5465. my($to_hdr) = qquote_rfc2821_local($msginfo->sender_contact);
  5466. # use the provided template text
  5467. my(%mybuiltins) = %$builtins_ref; # make a local copy
  5468. # not really needed, these header fields are overridden later
  5469. $mybuiltins{'f'} = c('hdrfrom_notify_sender');
  5470. $mybuiltins{'T'} = $to_hdr;
  5471. $mybuiltins{'d'} = rfc2822_timestamp($dsn_time);
  5472. my($dsn) = expand($template_ref, \%mybuiltins);
  5473. my($dsn_entity) = string_to_mime_entity($dsn);
  5474. $dsn_entity->make_multipart;
  5475. my($head) = $dsn_entity->head;
  5476. # rfc3464: The From field of the message header of the DSN SHOULD contain
  5477. # the address of a human who is responsible for maintaining the mail system
  5478. # at the Reporting MTA site (e.g. Postmaster), so that a reply to the
  5479. # DSN will reach that person.
  5480. # Override header fields from the template:
  5481. eval { $head->replace('From', c('hdrfrom_notify_sender')); 1 }
  5482. or do { chomp($@); die $@ };
  5483. eval { $head->replace('To', $to_hdr); 1 } or do { chomp($@); die $@ };
  5484. eval { $head->replace('Date', rfc2822_timestamp($dsn_time)); 1 }
  5485. or do { chomp($@); die $@ };
  5486. my($field) = Mail::Field->new('Content_type'); # underline, not hyphen!
  5487. $field->type("multipart/report; report-type=delivery-status");
  5488. $field->boundary(MIME::Entity::make_boundary());
  5489. $head->replace('Content-type', $field->stringify);
  5490. $head = undef;
  5491. # make sure _our_ source line number is reported in case of failure
  5492. eval {$dsn_entity->attach(
  5493. Type => 'message/delivery-status', Encoding => '7bit',
  5494. Description => 'Delivery error report',
  5495. Data => $msg); 1} or do {chomp($@); die $@};
  5496. eval {$dsn_entity->attach(
  5497. Type => 'text/rfc822-headers', Encoding => '-SUGGEST',
  5498. Description => 'Undelivered-message headers',
  5499. Data => $msginfo->orig_header); 1} or do {chomp($@); die $@};
  5500. $notification = Amavis::In::Message->new;
  5501. $notification->rx_time($dsn_time);
  5502. # $notification->body_type('7BIT');
  5503. $notification->delivery_method(c('notify_method'));
  5504. $notification->sender(c('mailfrom_notify_sender')); # should be empty!
  5505. $notification->auth_submitter('<>');
  5506. $notification->auth_user(c('amavis_auth_user'));
  5507. $notification->auth_pass(c('amavis_auth_pass'));
  5508. $notification->recips([$msginfo->sender_contact]);
  5509. $notification->mail_text($dsn_entity);
  5510. }
  5511. $notification;
  5512. }
  5513. # Return a triple of arrayrefs of quoted recipient addresses (the first lists
  5514. # recipients with successful delivery status, the second all the rest),
  5515. # plus a list of short per-recipient delivery reports for failed deliveries,
  5516. # that can be used in the first MIME part (the free text format) of delivery
  5517. # status notifications.
  5518. #
  5519. sub delivery_short_report($) {
  5520. my($msginfo) = @_;
  5521. my(@succ_recips, @failed_recips, @failed_recips_full);
  5522. for my $r (@{$msginfo->per_recip_data}) {
  5523. my($remote_mta) = $r->recip_remote_mta;
  5524. my($smtp_resp) = $r->recip_smtp_response;
  5525. my($qrecip_addr) = scalar(qquote_rfc2821_local($r->recip_addr));
  5526. if ($r->recip_destiny == D_PASS && ($smtp_resp=~/^2/ || !$r->recip_done)) {
  5527. push(@succ_recips, $qrecip_addr);
  5528. } else {
  5529. push(@failed_recips, $qrecip_addr);
  5530. push(@failed_recips_full, sprintf("%s:%s\n %s", $qrecip_addr,
  5531. (!defined($remote_mta)||$remote_mta eq '' ? '' : " $remote_mta said:"),
  5532. $smtp_resp));
  5533. }
  5534. }
  5535. (\@succ_recips, \@failed_recips, \@failed_recips_full);
  5536. }
  5537. # Build a new MIME::Entity object based on the original mail, but hopefully
  5538. # safer to mail readers: conventional mail header fields are retained,
  5539. # original mail becomes an attachment of type 'message/rfc822'.
  5540. # Text in $first_part becomes the first MIME part of type 'text/plain'.
  5541. #
  5542. sub defanged_mime_entity($$$) {
  5543. my($conn,$msginfo,$first_part) = @_;
  5544. my($new_entity);
  5545. $first_part = safe_encode(c('bdy_encoding'), $first_part);
  5546. # make sure _our_ source line number is reported in case of failure
  5547. my($nxmh) = c('notify_xmailer_header');
  5548. eval {$new_entity = MIME::Entity->build(
  5549. Type => 'multipart/mixed',
  5550. (defined $nxmh && $nxmh eq '' ? () # leave the MIME::Entity default
  5551. : ('X-Mailer' => $nxmh) ), # X-Mailer hdr or undef
  5552. ); 1} or do {chomp($@); die $@};
  5553. my($head) = $new_entity->head;
  5554. my($orig_head) = $msginfo->mime_entity->head;
  5555. # TODO: we should retain the ordering of Resent-* with their Received fields
  5556. for my $field_head ( # copy some of the original header fields
  5557. qw(Received From Sender To Cc Reply-To Date Message-ID
  5558. Resent-From Resent-Sender Resent-To Resent-Cc
  5559. Resent-Date Resent-Message-ID
  5560. In-Reply-To References Subject
  5561. Comments Keywords Organization X-Mailer) ) {
  5562. for my $value ($orig_head->get_all($field_head)) {
  5563. do_log(4, "copying-over the header field: $field_head");
  5564. eval { $head->add($field_head, $value); 1 } or do {chomp($@); die $@};
  5565. }
  5566. }
  5567. $head = undef; # object not needed any longer
  5568. eval {$new_entity->attach(
  5569. Type => 'text/plain', Encoding => '-SUGGEST', Charset => c('bdy_encoding'),
  5570. Data => $first_part); 1} or do {chomp($@); die $@};
  5571. eval {$new_entity->attach( # rfc2046
  5572. Type => 'message/rfc822; x-spam-type=original',
  5573. Encoding => '8bit', Path => $msginfo->mail_text_fn,
  5574. Description => 'Original message',
  5575. Filename => 'message.txt', Disposition => 'attachment'); 1}
  5576. or do {chomp($@); die $@};
  5577. $new_entity;
  5578. }
  5579. # Fill-in message object information based on a quarantined mail
  5580. sub msg_from_quarantine($$) {
  5581. my($conn,$msginfo) = @_;
  5582. my($fh) = $msginfo->mail_text;
  5583. my($fname) = $msginfo->mail_text_fn;
  5584. my($quarantine_id) = $msginfo->mail_id;
  5585. $msginfo->delivery_method(c('notify_method')); # c('forward_method') ???
  5586. $msginfo->auth_submitter('<>');
  5587. $msginfo->auth_user(c('amavis_auth_user'));
  5588. $msginfo->auth_pass(c('amavis_auth_pass'));
  5589. $fh->seek(0,0) or die "Can't rewind mail file: $!";
  5590. my($qid,$sender,@recips,$curr_head); my($ln); my($bsmtp) = 0;
  5591. # extract envelope information from the quarantine file
  5592. do_log(4, "msg_from_quarantine: releasing $quarantine_id");
  5593. for (undef $!; defined($ln=$fh->getline); undef $!) {
  5594. if ($ln =~ /^[ \t]/) { $curr_head .= $ln }
  5595. else {
  5596. my($next_head) = $ln; local($1,$2);
  5597. local($_) = $curr_head; chomp; s/\n([ \t])/$1/g; # unfold
  5598. if (!defined($curr_head)) { # first time
  5599. } elsif (/^(EHLO|HELO)( |$)/i) { $bsmtp = 1;
  5600. } elsif (/^MAIL FROM:\s*(<.*>)(.*)$/i) {
  5601. $bsmtp = 1; $sender = $1; $sender = unquote_rfc2821_local($sender);
  5602. } elsif ( $bsmtp && /^RCPT TO:\s*(<.*>)(.*)$/i) {
  5603. push(@recips, unquote_rfc2821_local($1));
  5604. } elsif ( $bsmtp && /^(DATA|NOOP)$/i) {
  5605. } elsif ( $bsmtp && /^RSET$/i) { $sender = undef; @recips = ();
  5606. } elsif (!$bsmtp && /^Return-Path:\s*(.*)$/i) {
  5607. } elsif (!$bsmtp && /^Delivered-To:\s*(.*)$/i) {
  5608. } elsif (!$bsmtp && /^X-Envelope-From:\s*<(.*)>$/i) {
  5609. $sender = $1; $sender = unquote_rfc2821_local($sender);
  5610. } elsif (!$bsmtp && /^X-Envelope-To:\s*(.*)$/i) {
  5611. my($to) = $1;
  5612. push(@recips, map {unquote_rfc2821_local($_)}
  5613. ($to =~ /\G < ([^>]*) > (?: , \s* )?/gcx) );
  5614. } elsif (/^X-Quarantine-Id:\s*(.*)$/i) {
  5615. $qid = $1; $qid = $1 if $qid =~ /^<(.*)>\z/s;
  5616. } else {
  5617. last; # end of known headers
  5618. }
  5619. last if $next_head eq "\n"; # end-of-header reached
  5620. $curr_head = $next_head;
  5621. }
  5622. }
  5623. defined $ln || $!==0 or die "Error reading file $fname: $!";
  5624. do_log(1,sprintf("Quarantined message: %s %s -> %s", $qid,
  5625. qquote_rfc2821_local($sender),
  5626. join(',', qquote_rfc2821_local(@recips)) ));
  5627. my(@m);
  5628. push(@m,'missing X-Quarantine-Id') if !defined $qid;
  5629. push(@m,'missing '.($bsmtp?'MAIL FROM':'X-Envelope-From')) if !defined $sender;
  5630. push(@m,'missing '.($bsmtp?'RCPT TO' :'X-Envelope-To')) if !@recips;
  5631. if (!defined($msginfo->sender)) { $msginfo->sender($sender) }
  5632. else { # sender specified in the request, overrides stored info
  5633. push(@m, sprintf("overriding sender %s by %s",
  5634. qquote_rfc2821_local($sender, $msginfo->sender) ));
  5635. }
  5636. if (!defined($msginfo->per_recip_data)) { $msginfo->recips(\@recips) }
  5637. else { # recipients specified in the request, overrides stored info
  5638. push(@m, sprintf("overriding recips %s by %s",
  5639. join(',', qquote_rfc2821_local(@recips)),
  5640. join(',', qquote_rfc2821_local(@{$msginfo->recips})) ));
  5641. }
  5642. do_log(0, "Quarantine release $quarantine_id: ".join("; ",@m)) if @m;
  5643. my($hdr_edits) = Amavis::Out::EditHeader->new;
  5644. for my $h (qw(Return-Path Delivered-To X-Quarantine-Id
  5645. X-Envelope-From X-Envelope-To X-Amavis-Hold))
  5646. { $hdr_edits->delete_header($h) }
  5647. $hdr_edits->prepend_header('Received',
  5648. received_line($conn,$msginfo,am_id(),1), 1);
  5649. # prepend Resent-* header fields, they must precede
  5650. # corresponding Received header field (pushed in reverse order)
  5651. # "Resent-From:" and "Resent-Date:" are required fields!
  5652. $hdr_edits->prepend_header('Resent-Message-ID',
  5653. sprintf('<QR%s@%s>',$msginfo->mail_id,$myhostname) );
  5654. $hdr_edits->prepend_header('Resent-Date', # time of the release request
  5655. rfc2822_timestamp($msginfo->rx_time));
  5656. $hdr_edits->prepend_header('Resent-To',
  5657. @{$msginfo->recips} != 1 ? 'undisclosed-recipients:;'
  5658. : qquote_rfc2821_local(@{$msginfo->recips}));
  5659. if ($msginfo->requested_by eq '') {
  5660. $hdr_edits->prepend_header('Resent-From', c('hdrfrom_notify_recip'));
  5661. } else {
  5662. $hdr_edits->prepend_header('Resent-Sender', c('hdrfrom_notify_recip'));
  5663. $hdr_edits->prepend_header('Resent-From',
  5664. qquote_rfc2821_local($msginfo->requested_by));
  5665. }
  5666. $msginfo->header_edits($hdr_edits);
  5667. if ($qid ne $quarantine_id)
  5668. { die "Stored quarantine ID '$qid' does not match ".
  5669. "requested ID '$quarantine_id'" }
  5670. if ($bsmtp)
  5671. { die "Releasing messages in BSMTP format not yet supported ".
  5672. "(dot stuffing not implemented)" }
  5673. $msginfo;
  5674. }
  5675. 1;
  5676. #
  5677. package Amavis::Cache;
  5678. # offer an 'IPC::Cache'-compatible simple interface
  5679. # to a local (per-process) memory-based cache;
  5680. use strict;
  5681. use re 'taint';
  5682. BEGIN {
  5683. import Amavis::Util qw(ll do_log);
  5684. }
  5685. BEGIN {
  5686. use Exporter ();
  5687. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  5688. $VERSION = '2.0431';
  5689. @ISA = qw(Exporter);
  5690. }
  5691. # simple local memory-based cache
  5692. sub new { # called by each child process
  5693. my($class) = @_;
  5694. do_log(5,"BerkeleyDB-based Amavis::Cache not available, ".
  5695. "using memory-based local cache");
  5696. bless {}, $class;
  5697. }
  5698. sub get { my($self,$key) = @_; thaw($self->{$key}) }
  5699. sub set { my($self,$key,$obj) = @_; $self->{$key} = freeze($obj) }
  5700. # protect % and ~, as well as NUL and \200 for good measure
  5701. sub encode($) {
  5702. my($str) = @_; $str =~ s/[%~\000\200]/sprintf("%%%02X",ord($&))/egs; $str;
  5703. }
  5704. # simple Storable::freeze lookalike
  5705. sub freeze($); # prototype
  5706. sub freeze($) {
  5707. my($obj) = @_; my($ty) = ref($obj);
  5708. if (!defined($obj)) { 'U' }
  5709. elsif (!$ty) { join('~', '', encode($obj)) } # string
  5710. elsif ($ty eq 'SCALAR') { join('~', 'S', encode(freeze($$obj))) }
  5711. elsif ($ty eq 'REF') { join('~', 'R', encode(freeze($$obj))) }
  5712. elsif ($ty eq 'ARRAY') { join('~', 'A', map {encode(freeze($_))} @$obj) }
  5713. elsif ($ty eq 'HASH') {
  5714. join('~','H',map {(encode($_),encode(freeze($obj->{$_})))} sort keys %$obj)
  5715. } else { die "Can't freeze object type $ty" }
  5716. }
  5717. # simple Storable::thaw lookalike
  5718. sub thaw($); # prototype
  5719. sub thaw($) {
  5720. my($str) = @_;
  5721. return undef if !defined $str;
  5722. my($ty,@val) = split(/~/,$str,-1);
  5723. for (@val) { s/%([0-9a-fA-F]{2})/pack("C",hex($1))/eg }
  5724. if ($ty eq 'U') { undef }
  5725. elsif ($ty eq '') { $val[0] }
  5726. elsif ($ty eq 'S') { my($obj)=thaw($val[0]); \$obj }
  5727. elsif ($ty eq 'R') { my($obj)=thaw($val[0]); \$obj }
  5728. elsif ($ty eq 'A') { [map {thaw($_)} @val] }
  5729. elsif ($ty eq 'H') {
  5730. my($hr) = {};
  5731. while (@val) { my($k) = shift @val; $hr->{$k} = thaw(shift @val) }
  5732. $hr;
  5733. } else { die "Can't thaw object type $ty" }
  5734. }
  5735. 1;
  5736. #
  5737. package Amavis;
  5738. require 5.005; # need qr operator and \z in regexps
  5739. use strict;
  5740. use re 'taint';
  5741. use Errno qw(ENOENT EACCES);
  5742. use POSIX qw(locale_h);
  5743. use IO::File ();
  5744. use Time::HiRes ();
  5745. # body digest for caching, either SHA1 or MD5
  5746. #use Digest::SHA1;
  5747. use Digest::MD5;
  5748. use Net::Server 0.83;
  5749. use Net::Server::PreForkSimple;
  5750. BEGIN {
  5751. import Amavis::Conf qw(:platform :sa :confvars c cr ca);
  5752. import Amavis::Util qw(untaint min max ll do_log sanitize_str debug_oneshot
  5753. am_id add_entropy generate_mail_id
  5754. snmp_counters_init snmp_count prolong_timer);
  5755. import Amavis::Log qw(open_log close_log);
  5756. import Amavis::Timing qw(section_time get_time_so_far);
  5757. import Amavis::rfc2821_2822_Tools;
  5758. import Amavis::Lookup qw(lookup);
  5759. import Amavis::Lookup::IP qw(lookup_ip_acl);
  5760. import Amavis::Out;
  5761. import Amavis::Out::EditHeader;
  5762. import Amavis::UnmangleSender qw(best_try_originator_ip best_try_originator
  5763. first_received_from);
  5764. import Amavis::Unpackers::Validity qw(
  5765. check_header_validity check_for_banned_names);
  5766. import Amavis::Unpackers::MIME qw(mime_decode);
  5767. import Amavis::Expand qw(expand);
  5768. import Amavis::Notify qw(delivery_status_notification delivery_short_report
  5769. string_to_mime_entity defanged_mime_entity);
  5770. import Amavis::In::Connection;
  5771. import Amavis::In::Message;
  5772. }
  5773. # Make it a subclass of Net::Server::PreForkSimple
  5774. # to override method &process_request (and others if desired)
  5775. use vars qw(@ISA);
  5776. # @ISA = qw(Net::Server);
  5777. @ISA = qw(Net::Server::PreForkSimple);
  5778. add_entropy(Time::HiRes::gettimeofday, $$, $], @INC, %ENV);
  5779. delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};
  5780. use vars qw(
  5781. $extra_code_db $extra_code_cache
  5782. $extra_code_sql_base $extra_code_sql_log $extra_code_sql_quar
  5783. $extra_code_sql_lookup $extra_code_ldap
  5784. $extra_code_in_amcl $extra_code_in_smtp
  5785. $extra_code_antivirus $extra_code_antispam $extra_code_unpackers);
  5786. use vars qw(%modules_basic);
  5787. use vars qw($spam_level $spam_status $spam_report);
  5788. use vars qw($user_id_sql $wb_listed_sql $implicit_maps_inserted);
  5789. use vars qw($db_env $snmp_db);
  5790. use vars qw($body_digest_cache);
  5791. use vars qw(%builtins); # customizable notification messages
  5792. use vars qw($child_invocation_count $child_task_count);
  5793. # $child_invocation_count # counts child re-use from 1 to max_requests
  5794. # $child_task_count # counts check_mail_begin_task (and check_mail) calls;
  5795. # this often runs in sync with $child_invocation_count,
  5796. # but with SMTP or LMTP input there may be more than one
  5797. # message passed during a single SMTP session
  5798. use vars qw(@config_files);
  5799. use vars qw($CONN $MSGINFO);
  5800. use vars qw($av_output @virusname @detecting_scanners
  5801. $banned_filename_any $banned_filename_all @bad_headers);
  5802. use vars qw($amcl_in_obj $smtp_in_obj); # Amavis::In::AMCL and In::SMTP objects
  5803. use vars qw($sql_dataset_conn_lookups); # Amavis::Out::SQL::Connection object
  5804. use vars qw($sql_dataset_conn_storage); # Amavis::Out::SQL::Connection object
  5805. use vars qw($sql_storage); # Amavis::Out::SQL::Log object
  5806. use vars qw($sql_policy $sql_wblist); # Amavis::Lookup::SQL objects
  5807. use vars qw($ldap_connection); # Amavis::LDAP::Connection object
  5808. use vars qw($ldap_policy); # Amavis::Lookup::LDAP object
  5809. # initialize the %builtins, which is an associative array of built-in macros
  5810. # to be used in notification message expansion.
  5811. sub init_builtin_macros() {
  5812. # A key (macro name) must be a single character. Most characters are
  5813. # allowed, but to be on the safe side and for clarity it is suggested
  5814. # that only letters are used. Upper case letters may (as a mnemonic)
  5815. # suggest the value is an array, lower case may suggest the value is
  5816. # a scalar string - but this is only a convention and not enforced.
  5817. #
  5818. # A value may be a reference to a subroutine which will be called later at
  5819. # the time of macro expansion. This way we can provide a method for obtaining
  5820. # information which is not yet available at the time of initialization, such
  5821. # as AV scanner results, or provide a lazy evaluation for more expensive
  5822. # calculations. Subroutine will be called in scalar context with no args.
  5823. # It may return a scalar string (or undef), or an array reference.
  5824. #
  5825. %builtins = (
  5826. '.' => undef,
  5827. p => sub {c('policy_bank_path')},
  5828. # mail reception timestamp (e.g. start of a SMTP transaction):
  5829. d => sub {rfc2822_timestamp($MSGINFO->rx_time)}, # rfc2822 local date-time
  5830. # U => sub {iso8601_timestamp($MSGINFO->rx_time)}, # iso8601, local time
  5831. U => sub {iso8601_utc_timestamp($MSGINFO->rx_time)}, # iso8601 UTC
  5832. y => sub {sprintf("%.0f", 1000*get_time_so_far())}, # elapsed time in ms
  5833. u => sub {sprintf("%010d",$MSGINFO->rx_time)}, # s since Unix epoch (UTC)
  5834. h => $myhostname, # dns name of this host, or configurable name
  5835. l => sub {my($ip) = $MSGINFO->client_addr; my($val);
  5836. $val = $ip ne '' ? lookup_ip_acl($ip,@{ca('mynetworks_maps')})
  5837. : lookup(0,$MSGINFO->sender_source,
  5838. @{ca('local_domains_maps')});
  5839. $val ? 1 : undef}, # sender's client IP (if known) from @mynetworks
  5840. # (if IP is known), or sender domain is local
  5841. s => sub {qquote_rfc2821_local($MSGINFO->sender)}, # original envelope sender in <>
  5842. S => sub { # unmangled sender or sender address to be notified, or empty...
  5843. sanitize_str($MSGINFO->sender_contact) }, # ..if sender unknown
  5844. o => sub { # best attempt at determining true sender (origin) of the virus,
  5845. sanitize_str($MSGINFO->sender_source) }, # normally same as %s
  5846. R => sub {$MSGINFO->recips}, # original message recipients list
  5847. D => sub {my($y,$n,$f)=delivery_short_report($MSGINFO); $y}, # succ.delivered
  5848. O => sub {my($y,$n,$f)=delivery_short_report($MSGINFO); $n}, # failed recips
  5849. N => sub {my($y,$n,$f)=delivery_short_report($MSGINFO); $f}, # short dsn
  5850. Q => sub {$MSGINFO->queue_id}, # MTA queue ID of the message if known
  5851. m => sub { local($_) = $MSGINFO->mime_entity; # Message-ID of the message
  5852. if (defined) { $_ = $_->head->get('Message-ID',0);
  5853. if (defined) {
  5854. chomp; s/^[ \t]+//; s/[ \t\n]+\z//; # trim
  5855. # protect space and \n, other special chars...
  5856. # ...will be sanitized before logging
  5857. s{([ =\r\n])}{sprintf("=%02X",ord($1))}eg;
  5858. }; $_ }},
  5859. r => sub { local($_) = $MSGINFO->mime_entity; # first Resent-Message-ID
  5860. if (defined) { $_ = $_->head->get('Resent-Message-ID',0);
  5861. if (defined) {
  5862. chomp; s/^[ \t]+//; s/[ \t\n]+\z//; # trim
  5863. s{([ =\r\n])}{sprintf("=%02X",ord($1))}eg;
  5864. }; $_ }},
  5865. j => sub { local($_) = $MSGINFO->mime_entity; # Subject of the message
  5866. if (defined) { $_ = $_->head->get('Subject',0); chomp;
  5867. s/\n([ \t])/$1/g; # unfold
  5868. s{([=\r\n])}{sprintf("=%02X",ord($1))}eg; $_ }},
  5869. b => sub {$MSGINFO->body_digest}, # original message body digest
  5870. n => \&am_id, # amavis internal message id (for log entries)
  5871. i => sub {$MSGINFO->mail_id}, # long-term unique mail id on this system
  5872. q => sub {my($q) = $MSGINFO->quarantined_to;
  5873. !defined($q) ? undef :
  5874. [map { my($m)=$_; $m=~s{^\Q$QUARANTINEDIR\E/}{}; $m } @$q];
  5875. }, # list of quarantine mailboxes
  5876. v => sub {[split(/[ \t]*\r?\n/,$av_output)]}, # anti-virus scanner output
  5877. V => sub {my(%seen); [grep {!$seen{$_}++} @virusname]}, #unique virus names
  5878. F => sub { my(%seen); # list of banned file names
  5879. my(@b) = grep { !$seen{$_}++ }
  5880. map { @{$_->banned_parts} }
  5881. grep { defined $_->banned_parts }
  5882. @{$MSGINFO->per_recip_data};
  5883. my($b_chopped) = @b > 2; @b = (@b[0,1],'...') if $b_chopped;
  5884. s/[ \t]{6,}/ ... /g for @b;
  5885. \@b },
  5886. X => sub {\@bad_headers}, # list of header syntax violations
  5887. W => sub {\@detecting_scanners}, # list of av scanners detecting a virus
  5888. H => sub {[map {my $h=$_; chomp($h); $h} @{$MSGINFO->orig_header}]},# orig hdr
  5889. A => sub {[split(/\r?\n/, $spam_report)]}, # SpamAssassin report lines
  5890. c => sub { if (!defined($spam_level)) { '-' }
  5891. else { # format SA score +/- by-sender score boosts
  5892. my($sl) = 0+sprintf("%.3f",$spam_level); # trim down fraction
  5893. my(@boost) = map { my($b) = $_->recip_score_boost;
  5894. !defined($b) ? undef : 0+sprintf("%.3f",$b)
  5895. } @{$MSGINFO->per_recip_data};
  5896. !(grep { defined($_) && $_ != 0 } @boost) ? $sl
  5897. : @boost==1 ? ($boost[0]>=0 ?$sl.'+'.$boost[0] :$sl.$boost[0])
  5898. : $sl . '+(' . join(',',@boost) . ')';
  5899. }
  5900. },
  5901. z => sub {$MSGINFO->msg_size}, # mail size
  5902. t => sub { # first entry in the Received trace
  5903. sanitize_str(first_received_from($MSGINFO->mime_entity)) },
  5904. e => sub { # first valid public IP in the Received trace
  5905. sanitize_str(best_try_originator_ip($MSGINFO->mime_entity)) },
  5906. a => sub {$MSGINFO->client_addr}, # original SMTP session client IP address
  5907. g => sub { # original SMTP session client DNS name
  5908. sanitize_str($MSGINFO->client_name) },
  5909. k => sub { my($kill_level);
  5910. scalar(grep # any recipient declared the message be killed ?
  5911. { !$_->recip_whitelisted_sender &&
  5912. ($_->recip_blacklisted_sender ||
  5913. ($kill_level=lookup(0,$_->recip_addr,
  5914. @{ca('spam_kill_level_maps')}),
  5915. defined $spam_level && defined $kill_level &&
  5916. $spam_level + $_->recip_score_boost >= $kill_level) )
  5917. } @{$MSGINFO->per_recip_data}) },
  5918. '1'=> sub { my($tag_level);
  5919. scalar(grep # above tag level for any recipient?
  5920. { !$_->recip_whitelisted_sender &&
  5921. ($_->recip_blacklisted_sender ||
  5922. ($tag_level=lookup(0,$_->recip_addr,
  5923. @{ca('spam_tag_level_maps')}),
  5924. defined $spam_level && defined $tag_level &&
  5925. $spam_level + $_->recip_score_boost >= $tag_level) )
  5926. } @{$MSGINFO->per_recip_data}) },
  5927. '2'=> sub { my($tag2_level);
  5928. scalar(grep # above tag2 level for any recipient?
  5929. { !$_->recip_whitelisted_sender &&
  5930. ($_->recip_blacklisted_sender ||
  5931. ($tag2_level=lookup(0,$_->recip_addr,
  5932. @{ca('spam_tag2_level_maps')}),
  5933. defined $spam_level && defined $tag2_level &&
  5934. $spam_level + $_->recip_score_boost >= $tag2_level) )
  5935. } @{$MSGINFO->per_recip_data}) },
  5936. # macros f, T, C, B will be defined for each notification as appropriate
  5937. # (representing From:, To:, Cc:, and Bcc: respectively)
  5938. # remaining free letters: wxyEGIJKLMPYZ
  5939. );
  5940. }
  5941. # initialize %local_delivery_aliases
  5942. sub init_local_delivery_aliases() {
  5943. # The %local_delivery_aliases maps local virtual 'localpart' to a mailbox
  5944. # (e.g. to a quarantine filename or a directory). Used by method 'local:',
  5945. # i.e. in mail_to_local_mailbox(), for direct local quarantining.
  5946. # The hash value may be a ref to a pair of fixed strings, or a subroutine ref
  5947. # (which must return a pair of strings (a list, not a list ref)) which makes
  5948. # possible lazy evaluation when some part of the pair is not known before
  5949. # the final delivery time. The first string in a pair must be either:
  5950. # - empty or undef, which will disable saving the message,
  5951. # - a filename, indicating a Unix-style mailbox,
  5952. # - a directory name, indicating a maildir-style mailbox,
  5953. # in which case the second string may provide a suggested file name.
  5954. #
  5955. %Amavis::Conf::local_delivery_aliases = (
  5956. 'virus-quarantine' => sub { ($QUARANTINEDIR, undef) },
  5957. 'banned-quarantine' => sub { ($QUARANTINEDIR, undef) },
  5958. 'bad-header-quarantine' => sub { ($QUARANTINEDIR, undef) },
  5959. 'spam-quarantine' => sub { ($QUARANTINEDIR, undef) },
  5960. # some more examples:
  5961. 'archive-files' => sub { ("$QUARANTINEDIR", undef) },
  5962. 'archive-mbox' => sub { ("$QUARANTINEDIR/archive.mbox", undef) },
  5963. 'recip-quarantine' => sub { ("$QUARANTINEDIR/recip-archive.mbox",undef) },
  5964. 'sender-quarantine' =>
  5965. sub { my($s) = $MSGINFO->sender;
  5966. $s = substr($s,0,100)."..." if length($s) > 100+3;
  5967. $s =~ tr/a-zA-Z0-9@._+-]/=/c; $s =~ s/\@/_at_/g;
  5968. $s = untaint($s) if $s =~ /^(?:[a-zA-Z0-9%=._+-]+)\z/; # untaint
  5969. ($QUARANTINEDIR, "sender-$s-%m.gz"); # suggested file name
  5970. },
  5971. # 'recip-quarantine2' => sub {
  5972. # my(@fnames);
  5973. # my($myfield) =
  5974. # Amavis::Lookup::SQLfield->new($sql_policy,'some_field_name','S');
  5975. # for my $r (@{$MSGINFO->recips}) {
  5976. # my($field_value) = lookup(0,$r,$myfield);
  5977. # my($fname) = $field_value; # or perhaps: my($fname) = $r;
  5978. # local($1); $fname =~ s/[^a-zA-Z0-9._@]/=/g; $fname =~ s/\@/%/g;
  5979. # $fname = untaint($fname) if $fname =~ /^([a-zA-Z0-9._=%]+)\z/;
  5980. # $fname =~ s/%/%%/g; # protect %
  5981. # do_log(3, "Recipient: $r, field: $field_value, fname: $fname");
  5982. # push(@fnames, $fname);
  5983. # }
  5984. # # ???what file name to choose if there is more than one recipient???
  5985. # ( $QUARANTINEDIR, "sender-$fnames[0]-%i-%n.gz" ); # suggested file name
  5986. # },
  5987. );
  5988. }
  5989. # initialize some remaining global variables;
  5990. # invoked after chroot and after privileges have been dropped
  5991. sub after_chroot_init() {
  5992. $child_invocation_count = $child_task_count = 0;
  5993. %modules_basic = %INC; # helps to track missing modules in chroot
  5994. my(@msg);
  5995. my($euid) = $>; # effective UID
  5996. $> = 0; # try to become root
  5997. POSIX::setuid(0) if $> != 0; # and try some more
  5998. if ($> == 0) { # succeded? panic!
  5999. @msg = ("It is possible to change EUID from $euid to root, ABORTING!",
  6000. "Please use the most recent Net::Server or apply a patch - see:",
  6001. " http://www.ijs.si/software/amavisd/#net-server-sec",
  6002. "or start as non-root, e.g. by su(1) or using option -u user");
  6003. } elsif ($daemon_chroot_dir eq '') {
  6004. # A quick check on vulnerability/protection of a config file
  6005. # (non-exhaustive: doesn't test for symlink tricks and higher directories).
  6006. # The config file has already been executed by now, so it may be
  6007. # too late to feel sorry now, but better late then never.
  6008. for my $config_file (@config_files) {
  6009. my($fh) = IO::File->new;
  6010. my($errn) = lstat($config_file) ? 0 : 0+$!;
  6011. if ($errn) { # not accessible, don't bother to test further
  6012. } elsif ($fh->open($config_file,'+<')) {
  6013. push(@msg, "Config file \"$config_file\" is writable, ".
  6014. "UID $<, EUID $>, EGID $)" );
  6015. $fh->close; # close, ignoring status
  6016. } elsif (rename($config_file, $config_file.'.moved')) {
  6017. my($m) = 'appears writable (unconfirmed)';
  6018. if (!-e($config_file) && -e($config_file.'.moved')) {
  6019. rename($config_file.'.moved', $config_file); # try to rename back
  6020. $m = 'is writable (confirmed)';
  6021. }
  6022. push(@msg, "Directory of a config file \"$config_file\" $m, ".
  6023. "UID $<, EUID $>, EGID $)" );
  6024. }
  6025. last if @msg;
  6026. }
  6027. }
  6028. if (@msg) {
  6029. do_log(-3,"FATAL: $_") for @msg;
  6030. print STDERR (map {"$_\n"} @msg);
  6031. die "SECURITY PROBLEM, ABORTING";
  6032. exit 1; # just in case
  6033. }
  6034. # report versions of some modules
  6035. for my $m ('Amavis::Conf',
  6036. sort map { s/\.pm\z//; s[/][::]g; $_ } grep { /\.pm\z/ } keys %INC){
  6037. next if !grep { $_ eq $m } qw(Amavis::Conf
  6038. Archive::Tar Archive::Zip Compress::Zlib Convert::TNEF Convert::UUlib
  6039. MIME::Entity MIME::Parser MIME::Tools Mail::Header Mail::Internet
  6040. Mail::ClamAV Mail::SpamAssassin Mail::SpamAssassin::SpamCopURI URI
  6041. Razor2::Client::Version Mail::SPF::Query Authen::SASL
  6042. IO::Socket::INET6 Net::DNS Net::SMTP Net::Cmd Net::Server Net::LDAP
  6043. DBI DBD::mysql DBD::SQLite BerkeleyDB DB_File
  6044. SAVI Unix::Syslog Time::HiRes);
  6045. do_log(0, sprintf("Module %-19s %s", $m, $m->VERSION || '?'));
  6046. }
  6047. if (c('forward_method') eq '' && $extra_code_in_smtp) {
  6048. do_log(1,"forward_method in default policy bank is null (milter setup?), ".
  6049. "DISABLING SMTP-in AS A PRECAUTION");
  6050. $extra_code_in_smtp = undef;
  6051. }
  6052. do_log(0,"Amavis::DB code ".($extra_code_db ?'':" NOT")." loaded");
  6053. do_log(0,"Amavis::Cache code".($extra_code_cache ?'':" NOT")." loaded");
  6054. do_log(0,"SQL base code ".($extra_code_sql_base ?'':" NOT")." loaded");
  6055. do_log(0,"SQL::Log code ".($extra_code_sql_log ?'':" NOT")." loaded");
  6056. do_log(0,"SQL::Quarantine ".($extra_code_sql_quar ?'':" NOT")." loaded");
  6057. do_log(0,"Lookup::SQL code ".($extra_code_sql_lookup?'':" NOT")." loaded");
  6058. do_log(0,"Lookup::LDAP code ".($extra_code_ldap ?'':" NOT")." loaded");
  6059. do_log(0,"AM.PDP prot code ".($extra_code_in_amcl ?'':" NOT")." loaded");
  6060. do_log(0,"SMTP-in prot code ".($extra_code_in_smtp ?'':" NOT")." loaded");
  6061. do_log(0,"ANTI-VIRUS code ".($extra_code_antivirus?'':" NOT")." loaded");
  6062. do_log(0,"ANTI-SPAM code ".($extra_code_antispam ?'':" NOT")." loaded");
  6063. do_log(0,"Unpackers code ".($extra_code_unpackers?'':" NOT")." loaded");
  6064. # store policy names into 'policy_bank_name' fields, if not explicitly set
  6065. for my $name (keys %policy_bank) {
  6066. if (ref($policy_bank{$name}) eq 'HASH' &&
  6067. !exists($policy_bank{$name}{'policy_bank_name'})) {
  6068. $policy_bank{$name}{'policy_bank_name'} = $name;
  6069. $policy_bank{$name}{'policy_bank_path'} = $name;
  6070. }
  6071. }
  6072. };
  6073. # overlay the current policy bank by settings from the
  6074. # $policy_bank{$policy_bank_name}, or load the default policy bank (empty name)
  6075. sub load_policy_bank($) {
  6076. my($policy_bank_name) = @_;
  6077. if (!exists $policy_bank{$policy_bank_name}) {
  6078. do_log(-1,"policy bank \"$policy_bank_name\" does not exist, ignored");
  6079. } elsif ($policy_bank_name eq '') {
  6080. %current_policy_bank = %{$policy_bank{$policy_bank_name}};
  6081. do_log(4,'loaded base policy bank');
  6082. } else {
  6083. my($cpbp) = c('policy_bank_path'); # currently loaded bank
  6084. for my $k (keys %{$policy_bank{$policy_bank_name}}) {
  6085. do_log(-1,"loading policy bank \"$policy_bank_name\": ".
  6086. "unknown field \"$k\"") if !exists $current_policy_bank{$k};
  6087. $current_policy_bank{$k} = $policy_bank{$policy_bank_name}{$k};
  6088. }
  6089. $current_policy_bank{'policy_bank_path'} =
  6090. ($cpbp eq '' ? '' : $cpbp.'/') . $policy_bank_name;
  6091. do_log(2,sprintf('loaded policy bank "%s"%s', $policy_bank_name,
  6092. $cpbp eq '' ? '' : " over \"$cpbp\""));
  6093. }
  6094. }
  6095. ### Net::Server hook
  6096. ### This hook occurs in the parent (master) process after chroot,
  6097. ### change of user, and change of group has occured. It allows
  6098. ### for preparation before looping begins.
  6099. sub pre_loop_hook {
  6100. my($self) = @_;
  6101. local $SIG{CHLD} = 'DEFAULT';
  6102. eval {
  6103. after_chroot_init(); # the rest of the top-level initialization
  6104. # this needs to be done only after chroot, otherwise paths will be wrong
  6105. find_external_programs([split(/:/,$path,-1)]); # path, decoders, scanners
  6106. # do some sanity checking
  6107. my($name) = $TEMPBASE;
  6108. $name = "$daemon_chroot_dir $name" if $daemon_chroot_dir ne '';
  6109. my($errn) = stat($TEMPBASE) ? 0 : 0+$!;
  6110. if ($errn==ENOENT) { die "No TEMPBASE directory: $name" }
  6111. elsif ($errn) { die "TEMPBASE directory inaccessible, $!: $name" }
  6112. elsif (!-d _) { die "TEMPBASE is not a directory: $name" }
  6113. elsif (!-w _) { die "TEMPBASE directory is not writable: $name" }
  6114. if ($enable_global_cache && $extra_code_db) {
  6115. my($name) = $db_home;
  6116. $name = "$daemon_chroot_dir $name" if $daemon_chroot_dir ne '';
  6117. $errn = stat($db_home) ? 0 : 0+$!;
  6118. if ($errn == ENOENT) {
  6119. die "Please create an empty directory $name to hold a database".
  6120. " (config variable \$db_home)\n" }
  6121. elsif ($errn) { die "db_home inaccessible, $!: $name" }
  6122. elsif (!-d _) { die "db_home is not a directory : $name" }
  6123. elsif (!-w _) { die "db_home directory is not writable: $name" }
  6124. Amavis::DB::init(1);
  6125. }
  6126. if ($QUARANTINEDIR ne '') {
  6127. my($name) = $QUARANTINEDIR;
  6128. $name = "$daemon_chroot_dir $name" if $daemon_chroot_dir ne '';
  6129. $errn = stat($QUARANTINEDIR) ? 0 : 0+$!;
  6130. if ($errn == ENOENT) { } # ok
  6131. elsif ($errn) { die "QUARANTINEDIR inaccessible, $!: $name" }
  6132. elsif (-d _ && !-w _) { die "QUARANTINEDIR directory not writable: $name" }
  6133. }
  6134. Amavis::SpamControl::init() if $extra_code_antispam;
  6135. };
  6136. if ($@ ne '') {
  6137. chomp($@); my($msg) = "TROUBLE in pre_loop_hook: $@"; do_log(-2,$msg);
  6138. die ("Suicide (" . am_id() . ") " . $msg . "\n");
  6139. }
  6140. 1;
  6141. }
  6142. ### log routine Net::Server hook
  6143. ### (Sys::Syslog MUST NOT be specified as a value of 'log_file'!)
  6144. #
  6145. # Redirect Net::Server logging to use Amavis' do_log().
  6146. # The main reason is that Net::Server uses Sys::Syslog
  6147. # (and has two bugs in doing it, at least the Net-Server-0.82),
  6148. # and Amavis users are acustomed to Unix::Syslog.
  6149. sub write_to_log_hook {
  6150. my($self,$level,$msg) = @_;
  6151. my($prop) = $self->{server};
  6152. local $SIG{CHLD} = 'DEFAULT';
  6153. chomp($msg);
  6154. do_log(1, "Net::Server: " . $msg); # just call Amavis' traditional logging
  6155. 1;
  6156. }
  6157. ### user customizable Net::Server hook (Net::Server 0.88 or later),
  6158. ### hook occurs in the master process
  6159. sub run_n_children_hook {
  6160. Amavis::AV::sophos_savi_reload()
  6161. if $extra_code_antivirus && Amavis::AV::sophos_savi_stale();
  6162. add_entropy(Time::HiRes::gettimeofday);
  6163. }
  6164. ### compatibility with patched Net::Server by SAVI patch (Net::Server <= 0.87)
  6165. sub parent_fork_hook { my($self) = @_; $self->run_n_children_hook }
  6166. ### user customizable Net::Server hook
  6167. sub child_init_hook {
  6168. my($self) = @_;
  6169. local $SIG{CHLD} = 'DEFAULT';
  6170. $0 = 'amavisd (virgin child)';
  6171. my($inherited_entropy);
  6172. eval {
  6173. $db_env = $snmp_db = $body_digest_cache = undef; # just in case
  6174. Amavis::Timing::init(); snmp_counters_init();
  6175. close_log(); open_log(); # reopen syslog or log file to get per-process fd
  6176. if ($extra_code_db) {
  6177. $db_env = Amavis::DB->new; # get access to a bdb environment
  6178. $snmp_db = Amavis::DB::SNMP->new($db_env);
  6179. $snmp_db->register_proc('') if defined $snmp_db; # process alive & idle
  6180. my($var_ref) = $snmp_db->read_snmp_variables('entropy');
  6181. $inherited_entropy = $var_ref->[0] if $var_ref && @$var_ref;
  6182. }
  6183. # if $db_env is undef the Amavis::Cache::new creates a memory-based cache
  6184. $body_digest_cache = Amavis::Cache->new($db_env);
  6185. if ($extra_code_db) { # is it worth reporting the timing? (probably not)
  6186. section_time('bdb-open');
  6187. do_log(2, Amavis::Timing::report()); # report elapsed times
  6188. }
  6189. # Prepare permanent SQL dataset connection objects, does not connect yet!
  6190. # $sql_dataset_conn_lookups and $sql_dataset_conn_storage may be the
  6191. # same dataset (one connection used), or they may be separate objects,
  6192. # which will make separate connections to distinct datasets,
  6193. # possibly using different SQL engine types or servers
  6194. if ($extra_code_sql_lookup && @lookup_sql_dsn) {
  6195. $sql_dataset_conn_lookups =
  6196. Amavis::Out::SQL::Connection->new(@lookup_sql_dsn);
  6197. }
  6198. if ($extra_code_sql_log && @storage_sql_dsn) {
  6199. if (!$sql_dataset_conn_lookups || @storage_sql_dsn != @lookup_sql_dsn
  6200. || grep { $storage_sql_dsn[$_] ne $lookup_sql_dsn[$_] }
  6201. (0..$#storage_sql_dsn) )
  6202. { # DSN differs or no SQL lookups, storage needs its own connection
  6203. $sql_dataset_conn_storage =
  6204. Amavis::Out::SQL::Connection->new(@storage_sql_dsn);
  6205. do_log(2,"storage and lookups will use separate connections to SQL")
  6206. if $sql_dataset_conn_lookups;
  6207. } else { # same dataset, use the same database connection object
  6208. $sql_dataset_conn_storage = $sql_dataset_conn_lookups;
  6209. do_log(2,"storage and lookups will use the same connection to SQL");
  6210. }
  6211. }
  6212. # Make storage/lookup objs to hold DBI handles and 'prepared' statements.
  6213. $sql_storage = Amavis::Out::SQL::Log->new($sql_dataset_conn_storage)
  6214. if $sql_dataset_conn_storage;
  6215. $sql_policy = Amavis::Lookup::SQL->new($sql_dataset_conn_lookups,
  6216. 'sel_policy') if $sql_dataset_conn_lookups;
  6217. $sql_wblist = Amavis::Lookup::SQL->new($sql_dataset_conn_lookups,
  6218. 'sel_wblist') if $sql_dataset_conn_lookups;
  6219. };
  6220. if ($@ ne '') {
  6221. chomp($@); do_log(-2, "TROUBLE in child_init_hook: $@");
  6222. die "Suicide in child_init_hook: $@\n";
  6223. }
  6224. add_entropy($$, Time::HiRes::gettimeofday, $inherited_entropy);
  6225. Amavis::Timing::go_idle('vir');
  6226. }
  6227. ### user customizable Net::Server hook
  6228. sub post_accept_hook {
  6229. my($self) = @_;
  6230. local $SIG{CHLD} = 'DEFAULT';
  6231. $child_invocation_count++;
  6232. $0 = sprintf("amavisd (ch%d-accept)", $child_invocation_count);
  6233. Amavis::Timing::go_busy('hi ');
  6234. # establish initial time right after 'accept'
  6235. Amavis::Timing::init(); snmp_counters_init();
  6236. $snmp_db->register_proc('A') if defined $snmp_db; # in 'accept' state
  6237. load_policy_bank(''); # start with a builting policy bank
  6238. }
  6239. ### user customizable Net::Server hook
  6240. ### if this hook returns 1 the request is processed
  6241. ### if this hook returns 0 the request is denied
  6242. sub allow_deny_hook {
  6243. my($self) = @_;
  6244. local($1,$2,$3,$4); # Perl bug: $1 and $2 come tainted from Net::Server !
  6245. local $SIG{CHLD} = 'DEFAULT';
  6246. my($prop) = $self->{server}; my($sock) = $prop->{client}; my($bank_name);
  6247. my($is_ux) = UNIVERSAL::can($sock,'NS_proto') && $sock->NS_proto eq 'UNIX';
  6248. if ($is_ux) {
  6249. $bank_name = $interface_policy{"SOCK"}; # possibly undef
  6250. } else {
  6251. my($myif,$myport) = ($prop->{sockaddr}, $prop->{sockport});
  6252. if (defined $interface_policy{"$myif:$myport"}) {
  6253. $bank_name = $interface_policy{"$myif:$myport"};
  6254. } elsif (defined $interface_policy{$myport}) {
  6255. $bank_name = $interface_policy{$myport};
  6256. }
  6257. }
  6258. load_policy_bank($bank_name) if defined $bank_name &&
  6259. $bank_name ne c('policy_bank_name');
  6260. # note that the new policy bank may have replaced the inet_acl access table
  6261. if ($is_ux) {
  6262. # always permit access - unix sockets are immune to this check
  6263. } else {
  6264. my($permit,$fullkey,$err) = lookup_ip_acl($prop->{peeraddr},
  6265. Amavis::Lookup::Label->new('inet_acl'), ca('inet_acl'));
  6266. if (defined($err) && $err ne '') {
  6267. do_log(-1, sprintf("DENIED ACCESS due to INVALID IP ADDRESS %s: %s",
  6268. $prop->{peeraddr}, $err));
  6269. return 0;
  6270. } elsif (!$permit) {
  6271. my($msg) = sprintf("DENIED ACCESS from IP %s, policy bank '%s'",
  6272. $prop->{peeraddr}, c('policy_bank_name') );
  6273. $msg .= ", blocked by rule $fullkey" if defined $fullkey;
  6274. do_log(-1,$msg);
  6275. return 0;
  6276. }
  6277. }
  6278. 1;
  6279. }
  6280. # use Fcntl qw(F_GETFD F_SETFD FD_CLOEXEC);
  6281. # sub cloexec_on($;$) {
  6282. # my($fd,$name) = @_; my($flags);
  6283. # $flags = fcntl($fd, F_GETFD, 0)
  6284. # or die "Can't get flags from the file descriptor: $!";
  6285. # if ($flags & FD_CLOEXEC == 0) {
  6286. # do_log(4,"Turning on FD_CLOEXEC flag on $name");
  6287. # fcntl($fd, F_SETFD, $flags | FD_CLOEXEC)
  6288. # or die "Can't set FD_CLOEXEC on file descriptor $name: $!";
  6289. # }
  6290. # }
  6291. ### The heart of the program
  6292. ### user customizable Net::Server hook
  6293. sub process_request {
  6294. my($self) = shift;
  6295. my($prop) = $self->{server}; my($sock) = $prop->{client};
  6296. local $SIG{CHLD} = 'DEFAULT';
  6297. local($1,$2,$3,$4); # Perl bug: $1 and $2 come tainted from Net::Server !
  6298. # Net::Server assigns STDIN and STDOUT to the socket
  6299. binmode(STDIN) or die "Can't set STDIN to binmode: $!";
  6300. binmode(STDOUT) or die "Can't set STDOUT to binmode: $!";
  6301. binmode($sock) or die "Can't set socket to binmode: $!";
  6302. $| = 1;
  6303. local $SIG{ALRM} = sub { die "timed out\n" }; # do not modify the sig text!
  6304. eval {
  6305. # if ($] < 5.006) { # Perl older than 5.6.0 did not set FD_CLOEXEC on sockets
  6306. # for my $mysock (@{$prop->{sock}}) { cloexec_on($mysock, $mysock) }
  6307. # }
  6308. prolong_timer('new request - timer reset', $child_timeout); # timer init
  6309. if ($extra_code_ldap && !defined $ldap_policy) {
  6310. # make LDAP lookup object
  6311. $ldap_connection = Amavis::LDAP::Connection->new($default_ldap);
  6312. $ldap_policy = Amavis::Lookup::LDAP->new($default_ldap,$ldap_connection)
  6313. if $ldap_connection;
  6314. }
  6315. if (defined $ldap_policy && !$implicit_maps_inserted) {
  6316. # make LDAP field lookup objects with incorporated field names
  6317. # fieldtype: B=boolean, N=numeric, S=string, L=list
  6318. # B-, N-, S-, L- returns undef if field does not exist
  6319. # B0: boolean, nonexistent field treated as false,
  6320. # B1: boolean, nonexistent field treated as true
  6321. my $lf = sub{Amavis::Lookup::LDAPattr->new($ldap_policy,@_)};
  6322. unshift(@Amavis::Conf::virus_lovers_maps, $lf->('amavisVirusLover', 'B-'));
  6323. unshift(@Amavis::Conf::spam_lovers_maps, $lf->('amavisSpamLover', 'B-'));
  6324. unshift(@Amavis::Conf::banned_files_lovers_maps, $lf->('amavisBannedFilesLover', 'B-'));
  6325. unshift(@Amavis::Conf::bad_header_lovers_maps, $lf->('amavisBadHeaderLover', 'B-'));
  6326. unshift(@Amavis::Conf::bypass_virus_checks_maps, $lf->('amavisBypassVirusChecks', 'B-'));
  6327. unshift(@Amavis::Conf::bypass_spam_checks_maps, $lf->('amavisBypassSpamChecks', 'B-'));
  6328. unshift(@Amavis::Conf::bypass_banned_checks_maps,$lf->('amavisBypassBannedChecks', 'B-'));
  6329. unshift(@Amavis::Conf::bypass_header_checks_maps,$lf->('amavisBypassHeaderChecks', 'B-'));
  6330. unshift(@Amavis::Conf::spam_tag_level_maps, $lf->('amavisSpamTagLevel', 'N-'));
  6331. unshift(@Amavis::Conf::spam_tag2_level_maps, $lf->('amavisSpamTag2Level', 'N-'));
  6332. unshift(@Amavis::Conf::spam_kill_level_maps, $lf->('amavisSpamKillLevel', 'N-'));
  6333. unshift(@Amavis::Conf::spam_modifies_subj_maps, $lf->('amavisSpamModifiesSubj', 'B-'));
  6334. unshift(@Amavis::Conf::message_size_limit_maps, $lf->('amavisMessageSizeLimit', 'N-'));
  6335. unshift(@Amavis::Conf::virus_quarantine_to_maps, $lf->('amavisVirusQuarantineTo', 'S-'));
  6336. unshift(@Amavis::Conf::spam_quarantine_to_maps, $lf->('amavisSpamQuarantineTo', 'S-'));
  6337. unshift(@Amavis::Conf::banned_quarantine_to_maps, $lf->('amavisBannedQuarantineTo','S-'));
  6338. unshift(@Amavis::Conf::bad_header_quarantine_to_maps, $lf->('amavisBadHeaderQuarantineTo', 'S-'));
  6339. unshift(@Amavis::Conf::local_domains_maps, $lf->('amavisLocal', 'B1'));
  6340. unshift(@Amavis::Conf::warnvirusrecip_maps, $lf->('amavisWarnVirusRecip', 'B-'));
  6341. unshift(@Amavis::Conf::warnbannedrecip_maps, $lf->('amavisWarnBannedRecip', 'B-'));
  6342. unshift(@Amavis::Conf::warnbadhrecip_maps, $lf->('amavisWarnBadHeaderRecip', 'B-'));
  6343. unshift(@Amavis::Conf::virus_admin_maps, $lf->('amavisVirusAdmin', 'S-'));
  6344. unshift(@Amavis::Conf::newvirus_admin_maps, $lf->('amavisNewVirusAdmin', 'S-'));
  6345. unshift(@Amavis::Conf::spam_admin_maps, $lf->('amavisSpamAdmin', 'S-'));
  6346. unshift(@Amavis::Conf::banned_admin_maps, $lf->('amavisBannedAdmin', 'S-'));
  6347. unshift(@Amavis::Conf::bad_header_admin_maps, $lf->('amavisBadHeaderAdmin', 'S-'));
  6348. unshift(@Amavis::Conf::banned_filename_maps, $lf->('amavisBannedRuleNames', 'L-'));
  6349. section_time('ldap-prepare');
  6350. }
  6351. if (defined $sql_policy && !$implicit_maps_inserted) {
  6352. # make SQL field lookup objects with incorporated field names
  6353. # fieldtype: B=boolean, N=numeric, S=string,
  6354. # B-, N-, S- returns undef if field does not exist
  6355. # B0: boolean, nonexistent field treated as false,
  6356. # B1: boolean, nonexistent field treated as true
  6357. my $nf = sub{Amavis::Lookup::SQLfield->new($sql_policy,@_)}; #shorthand
  6358. $user_id_sql = $nf->('id', 'S');
  6359. unshift(@Amavis::Conf::local_domains_maps, $nf->('local', 'B1'));
  6360. unshift(@Amavis::Conf::virus_lovers_maps, $nf->('virus_lover', 'B-'));
  6361. unshift(@Amavis::Conf::spam_lovers_maps, $nf->('spam_lover', 'B-'));
  6362. unshift(@Amavis::Conf::banned_files_lovers_maps, $nf->('banned_files_lover', 'B-'));
  6363. unshift(@Amavis::Conf::bad_header_lovers_maps, $nf->('bad_header_lover', 'B-'));
  6364. unshift(@Amavis::Conf::bypass_virus_checks_maps, $nf->('bypass_virus_checks', 'B-'));
  6365. unshift(@Amavis::Conf::bypass_spam_checks_maps, $nf->('bypass_spam_checks', 'B-'));
  6366. unshift(@Amavis::Conf::bypass_banned_checks_maps, $nf->('bypass_banned_checks', 'B-'));
  6367. unshift(@Amavis::Conf::bypass_header_checks_maps, $nf->('bypass_header_checks', 'B-'));
  6368. unshift(@Amavis::Conf::spam_tag_level_maps, $nf->('spam_tag_level', 'N-'));
  6369. unshift(@Amavis::Conf::spam_tag2_level_maps, $nf->('spam_tag2_level', 'N-'));
  6370. unshift(@Amavis::Conf::spam_kill_level_maps, $nf->('spam_kill_level', 'N-'));
  6371. unshift(@Amavis::Conf::spam_dsn_cutoff_level_maps,$nf->('spam_dsn_cutoff_level','N-'));
  6372. unshift(@Amavis::Conf::spam_quarantine_cutoff_level_maps,$nf->('spam_quarantine_cutoff_level','N-'));
  6373. unshift(@Amavis::Conf::spam_modifies_subj_maps, $nf->('spam_modifies_subj', 'B-'));
  6374. unshift(@Amavis::Conf::spam_subject_tag_maps, $nf->('spam_subject_tag', 'S-'));
  6375. unshift(@Amavis::Conf::spam_subject_tag2_maps, $nf->('spam_subject_tag2', 'S-'));
  6376. unshift(@Amavis::Conf::virus_quarantine_to_maps, $nf->('virus_quarantine_to', 'S-'));
  6377. unshift(@Amavis::Conf::banned_quarantine_to_maps, $nf->('banned_quarantine_to', 'S-'));
  6378. unshift(@Amavis::Conf::bad_header_quarantine_to_maps, $nf->('bad_header_quarantine_to','S-'));
  6379. unshift(@Amavis::Conf::spam_quarantine_to_maps, $nf->('spam_quarantine_to', 'S-'));
  6380. unshift(@Amavis::Conf::message_size_limit_maps, $nf->('message_size_limit', 'N-'));
  6381. unshift(@Amavis::Conf::addr_extension_virus_maps, $nf->('addr_extension_virus', 'S-'));
  6382. unshift(@Amavis::Conf::addr_extension_spam_maps, $nf->('addr_extension_spam', 'S-'));
  6383. unshift(@Amavis::Conf::addr_extension_banned_maps,$nf->('addr_extension_banned','S-'));
  6384. unshift(@Amavis::Conf::addr_extension_bad_header_maps,$nf->('addr_extension_bad_header','S-'));
  6385. unshift(@Amavis::Conf::warnvirusrecip_maps, $nf->('warnvirusrecip', 'B-'));
  6386. unshift(@Amavis::Conf::warnbannedrecip_maps, $nf->('warnbannedrecip', 'B-'));
  6387. unshift(@Amavis::Conf::warnbadhrecip_maps, $nf->('warnbadhrecip', 'B-'));
  6388. unshift(@Amavis::Conf::newvirus_admin_maps, $nf->('newvirus_admin', 'S-'));
  6389. unshift(@Amavis::Conf::virus_admin_maps, $nf->('virus_admin', 'S-'));
  6390. unshift(@Amavis::Conf::banned_admin_maps, $nf->('banned_admin', 'S-'));
  6391. unshift(@Amavis::Conf::bad_header_admin_maps, $nf->('bad_header_admin', 'S-'));
  6392. unshift(@Amavis::Conf::spam_admin_maps, $nf->('spam_admin', 'S-'));
  6393. unshift(@Amavis::Conf::banned_filename_maps, $nf->('banned_rulenames', 'S-'));
  6394. section_time('sql-prepare');
  6395. }
  6396. Amavis::Conf::label_default_maps() if !$implicit_maps_inserted;
  6397. $implicit_maps_inserted = 1;
  6398. my($conn) = Amavis::In::Connection->new;
  6399. $CONN = $conn; # ugly - save in a global
  6400. $conn->proto($sock->NS_proto);
  6401. my($suggested_protocol) = c('protocol'); # suggested by the policy bank
  6402. ll(5) && do_log(5,"process_request: ".
  6403. "suggested_protocol=\"$suggested_protocol\" on ".$sock->NS_proto);
  6404. if ($sock->NS_proto eq 'UNIX') { # traditional amavis helper program
  6405. if ($suggested_protocol eq 'COURIER') {
  6406. die "unavailable support for protocol: $suggested_protocol";
  6407. } elsif ($suggested_protocol eq 'AM.PDP') {
  6408. $amcl_in_obj = Amavis::In::AMCL->new if !$amcl_in_obj;
  6409. $amcl_in_obj->process_policy_request($sock, $conn, \&check_mail, 0);
  6410. } else { # default to old amavis helper program protocol
  6411. $amcl_in_obj = Amavis::In::AMCL->new if !$amcl_in_obj;
  6412. $amcl_in_obj->process_policy_request($sock, $conn, \&check_mail, 1);
  6413. }
  6414. } elsif ($sock->NS_proto eq 'TCP') {
  6415. $conn->socket_ip($prop->{sockaddr});
  6416. $conn->socket_port($prop->{sockport});
  6417. $conn->client_ip($prop->{peeraddr});
  6418. if ($suggested_protocol eq 'TCP-LOOKUP') { # postfix maps (experimental)
  6419. process_tcp_lookup_request($sock, $conn);
  6420. do_log(2, Amavis::Timing::report()); # report elapsed times
  6421. } elsif ($suggested_protocol eq 'AM.PDP') {
  6422. # amavis policy delegation protocol (e.g. new milter helper program)
  6423. $amcl_in_obj = Amavis::In::AMCL->new if !$amcl_in_obj;
  6424. $amcl_in_obj->process_policy_request($sock, $conn, \&check_mail, 0);
  6425. } else { # defaults to SMTP or LMTP
  6426. if (!$extra_code_in_smtp) {
  6427. die "incoming TCP connection, but dynamic SMTP/LMTP code not loaded";
  6428. }
  6429. $smtp_in_obj = Amavis::In::SMTP->new if !$smtp_in_obj;
  6430. $smtp_in_obj->process_smtp_request(
  6431. $sock, ($suggested_protocol eq 'LMTP'?1:0), $conn, \&check_mail);
  6432. }
  6433. } else {
  6434. die ("unsupported protocol: $suggested_protocol, " . $sock->NS_proto);
  6435. }
  6436. }; # eval
  6437. alarm(0); # stop the timer
  6438. if ($@ ne '') {
  6439. chomp($@); my($timed_out) = $@ eq "timed out";
  6440. my($msg) = $timed_out ? "Child task exceeded $child_timeout seconds, abort"
  6441. : "TROUBLE in process_request: $@";
  6442. do_log(-2, $msg);
  6443. $smtp_in_obj->preserve_evidence(1) if $smtp_in_obj && !$timed_out;
  6444. # kills a child, hopefully preserving tempdir; does not kill parent
  6445. do_log(-1, "Requesting process rundown after fatal error");
  6446. $self->done(1);
  6447. # die ("Suicide (" . am_id() . ") " . $msg . "\n");
  6448. } elsif ($child_task_count >= $max_requests) {
  6449. # in case of multiple-transaction protocols (e.g. SMTP, LMTP)
  6450. # we do not like to keep running indefinitely at the mercy of MTA
  6451. do_log(2, "Requesting process rundown after $child_task_count tasks ".
  6452. "(and $child_invocation_count sessions)");
  6453. $self->done(1);
  6454. } elsif ($extra_code_antivirus && Amavis::AV::sophos_savi_stale() ) {
  6455. do_log(0, "Requesting process rundown due to stale Sophos virus data");
  6456. $self->done(1);
  6457. }
  6458. my(@modules_extra) = grep {!exists $modules_basic{$_}} keys %INC;
  6459. # do_log(0, "modules loaded: ".join(", ", sort keys %modules_basic));
  6460. do_log(1, "extra modules loaded: ".
  6461. join(", ", sort @modules_extra)) if @modules_extra;
  6462. }
  6463. ### override Net::Server::PreForkSimple::done (needed for Net::Server <= 0.87)
  6464. ### to be able to rundown the child process prematurely
  6465. sub done(@) {
  6466. my($self) = shift;
  6467. if (@_) { $self->{server}->{done} = shift }
  6468. elsif (!$self->{server}->{done})
  6469. { $self->{server}->{done} = $self->SUPER::done }
  6470. $self->{server}->{done};
  6471. }
  6472. ### Net::Server hook
  6473. sub post_process_request_hook {
  6474. my($self) = @_;
  6475. local $SIG{CHLD} = 'DEFAULT';
  6476. debug_oneshot(0);
  6477. $0 = sprintf("amavisd (ch%d-avail)", $child_invocation_count);
  6478. alarm(0); do_log(5,"post_process_request_hook: timer stopped");
  6479. $snmp_db->register_proc('') if defined $snmp_db; # process is alive and idle
  6480. Amavis::Timing::go_idle('bye'); Amavis::Timing::report_load();
  6481. }
  6482. ### Child is about to be terminated
  6483. ### user customizable Net::Server hook
  6484. sub child_finish_hook {
  6485. my($self) = @_;
  6486. local $SIG{CHLD} = 'DEFAULT';
  6487. # for my $m (sort map { s/\.pm\z//; s[/][::]g; $_ } grep { /\.pm\z/ } keys %INC){
  6488. # do_log(0, sprintf("Module %-19s %s", $m, $m->VERSION || '?'))
  6489. # if grep {$m=~/^$_/} qw(Mail::ClamAV Mail::SpamAssassin Razor2 Net::DNS);
  6490. # }
  6491. $0 = sprintf("amavisd (ch%d-finish)", $child_invocation_count);
  6492. do_log(5,"child_finish_hook: invoking DESTROY methods");
  6493. $smtp_in_obj = undef; # calls Amavis::In::SMTP::DESTROY
  6494. $amcl_in_obj = undef; # (currently does nothing for Amavis::In::AMCL)
  6495. $sql_storage = undef; # calls Amavis::Out::SQL::Log::DESTROY
  6496. $sql_wblist = undef; # calls Amavis::Lookup::SQL::DESTROY
  6497. $sql_policy = undef; # calls Amavis::Lookup::SQL::DESTROY
  6498. $ldap_policy = undef; # calls Amavis::Lookup::LDAP::DESTROY
  6499. # calls Amavis::Out::SQL::Connection::DESTROY
  6500. $sql_dataset_conn_lookups = $sql_dataset_conn_storage = undef;
  6501. # calls Amavis::LDAP::Connection::DESTROY
  6502. $ldap_connection = undef;
  6503. $body_digest_cache = undef; # calls Amavis::Cache::DESTROY
  6504. eval { $snmp_db->register_proc(undef) } if defined $snmp_db; # going away
  6505. $snmp_db = undef; # calls Amavis::DB::SNMP::DESTROY
  6506. $db_env = undef;
  6507. }
  6508. sub END { # runs before exiting the module
  6509. # do_log(5,"at the END handler: invoking DESTROY methods");
  6510. $smtp_in_obj = undef; # at end calls Amavis::In::SMTP::DESTROY
  6511. $amcl_in_obj = undef; # (currently does nothing for Amavis::In::AMCL)
  6512. $sql_storage = undef; # at end calls Amavis::Out::SQL::Log::DESTROY
  6513. $sql_wblist = undef; # at end calls Amavis::Lookup::SQL::DESTROY
  6514. $sql_policy = undef; # at end calls Amavis::Lookup::SQL::DESTROY
  6515. $ldap_policy = undef; # at end calls Amavis::Lookup::LDAP::DESTROY
  6516. # at end calls Amavis::Out::SQL::Connection::DESTROY
  6517. $sql_dataset_conn_lookups = $sql_dataset_conn_storage = undef;
  6518. # at end calls Amavis::LDAP::Connection::DESTROY
  6519. $ldap_connection = undef;
  6520. $body_digest_cache = undef; # at end calls Amavis::Cache::DESTROY
  6521. eval { $snmp_db->register_proc(undef) } if defined $snmp_db; # going away
  6522. $snmp_db = undef; # at end calls Amavis::DB::SNMP::DESTROY
  6523. $db_env = undef;
  6524. }
  6525. # implements Postfix TCP lookup server, see tcp_table(5) man page; experimental
  6526. sub process_tcp_lookup_request($$) {
  6527. my($sock, $conn) = @_;
  6528. local($/) = "\012"; # set line terminator to LF (regardless of platform)
  6529. my($req_cnt); my($ln);
  6530. for (undef $!; defined($ln=$sock->getline); undef $!) {
  6531. $req_cnt++; my($level) = 0;
  6532. my($resp_code, $resp_msg) = (400, 'INTERNAL ERROR');
  6533. if ($ln =~ /^get (.*?)\015?\012\z/si) {
  6534. my($key) = tcp_lookup_decode($1);
  6535. my($sl); $sl = lookup(0,$key, @{ca('spam_lovers_maps')});
  6536. $resp_code = 200; $level = 2;
  6537. $resp_msg = $sl ? "OK Recipient <$key> IS spam lover"
  6538. : "DUNNO Recipient <$key> is NOT spam lover";
  6539. } elsif ($ln =~ /^put ([^ ]*) (.*?)\015?\012\z/si) {
  6540. $resp_code = 500; $resp_msg = 'request not implemented: ' . $ln;
  6541. } else {
  6542. $resp_code = 500; $resp_msg = 'illegal request: ' . $ln;
  6543. }
  6544. do_log($level, "tcp_lookup($req_cnt): $resp_code $resp_msg");
  6545. $sock->printf("%03d %s\012", $resp_code, tcp_lookup_encode($resp_msg))
  6546. or die "Can't write to tcp_lookup socket: $!";
  6547. }
  6548. defined $ln || $!==0 or die "Error reading from socket: $!";
  6549. do_log(0, "tcp_lookup: RUNDOWN after $req_cnt requests");
  6550. }
  6551. sub tcp_lookup_encode($) {
  6552. my($str) = @_;
  6553. $str =~ s/[^\041-\044\046-\176]/sprintf("%%%02x",ord($&))/eg;
  6554. $str;
  6555. }
  6556. sub tcp_lookup_decode($) {
  6557. my($str) = @_;
  6558. $str =~ s/%([0-9a-fA-F]{2})/pack("C",hex($1))/eg;
  6559. $str;
  6560. }
  6561. sub check_mail_begin_task() {
  6562. # The check_mail_begin_task (and check_mail) may be called several times
  6563. # per child lifetime and/or per-SMTP session. The variable $child_task_count
  6564. # is mainly used by AV-scanner interfaces, e.g. to initialize when invoked
  6565. # for the first time during child process lifetime
  6566. $child_task_count++;
  6567. do_log(4, "check_mail_begin_task: task_count=$child_task_count");
  6568. # comment out to retain SQL/LDAP cache entries for the whole child lifetime:
  6569. $sql_policy->clear_cache if defined $sql_policy;
  6570. $sql_wblist->clear_cache if defined $sql_wblist;
  6571. $ldap_policy->clear_cache if defined $ldap_policy;
  6572. # reset certain global variables for each task
  6573. $av_output = undef; @detecting_scanners = ();
  6574. @virusname = (); @bad_headers = ();
  6575. $banned_filename_any = $banned_filename_all = 0;
  6576. $spam_level = undef; $spam_status = undef; $spam_report = undef;
  6577. $MSGINFO = undef; # just in case
  6578. }
  6579. # Checks the message stored on a file. File must already
  6580. # be open on file handle $msginfo->mail_text; it need not be positioned
  6581. # properly, check_mail must not close the file handle.
  6582. #
  6583. sub check_mail($$$) {
  6584. my($conn, $msginfo, $dsn_per_recip_capable) = @_;
  6585. my($point_of_no_return) = 0; # past the point where mail or DSN was sent
  6586. my($am_id) = am_id();
  6587. $snmp_db->register_proc($am_id) if defined $snmp_db;
  6588. my($tempdir) = $msginfo->mail_tempdir;
  6589. my($fh) = $msginfo->mail_text; my(@recips) = @{$msginfo->recips};
  6590. $MSGINFO = $msginfo; # ugly - save in a global, to make it accessible
  6591. # to %builtins
  6592. # compute body digest, measure mail size and check for 8-bit data
  6593. my($body_digest) = get_body_digest($fh, $msginfo);
  6594. my($mail_size) = $msginfo->msg_size; # use corrected ESMTP size if available
  6595. if ($mail_size <= 0) { # not available?
  6596. $mail_size = $msginfo->orig_header_size + 1 + $msginfo->orig_body_size;
  6597. $msginfo->msg_size($mail_size); # store back
  6598. }
  6599. my($file_generator_object) = # maxfiles 0 disables the $MAXFILES limit
  6600. Amavis::Unpackers::NewFilename->new($MAXFILES?$MAXFILES:undef, $mail_size);
  6601. Amavis::Unpackers::Part::init($file_generator_object); # fudge: keep in variable
  6602. my($parts_root) = Amavis::Unpackers::Part->new;
  6603. $msginfo->parts_root($parts_root);
  6604. my($smtp_resp, $exit_code, $preserve_evidence); my($virus_dejavu) = 0;
  6605. my($virus_presence_checked,$spam_presence_checked);
  6606. my($autolearn_status);
  6607. # matching banned rules suggest DSN to be suppressed?
  6608. my($banned_dsn_suppress) = 0;
  6609. # is any mail component password protected or otherwise non-decodable?
  6610. my($any_undecipherable) = 0;
  6611. my($mime_err); # undef, or MIME parsing error string as given by MIME::Parser
  6612. my($hold); # set to some string to cause the message to be placed on hold
  6613. # (frozen) by MTA. This can be used in cases when we stumble
  6614. # across some permanent problem making us unable to decide
  6615. # if the message is to be really delivered.
  6616. my($cl_ip) = $msginfo->client_addr;
  6617. add_entropy(Time::HiRes::gettimeofday,
  6618. "$child_task_count $am_id $cl_ip $mail_size", $msginfo->queue_id,
  6619. $msginfo->mail_text_fn, $msginfo->sender, $msginfo->recips);
  6620. my($mail_id);
  6621. my($which_section);
  6622. $which_section = 'gen_mail_id';
  6623. # create unique mail_id and save preliminary information to SQL (if enabled)
  6624. for (my($attempt)=5; $attempt>0; ) { # sanity limit on retries
  6625. my($secret_id);
  6626. ($mail_id,$secret_id) = generate_mail_id();
  6627. $msginfo->secret_id($secret_id); $secret_id = '';
  6628. $msginfo->mail_id($mail_id); # assign some long-term unique id to the msg
  6629. if (!$sql_storage) { last } # no need to store and to check for uniqueness
  6630. else { # attempt to save message placeholder to SQL ensuring it is unique
  6631. $which_section = 'sql-enter';
  6632. $sql_storage->save_info_preliminary($conn,$msginfo) and last;
  6633. if (--$attempt <= 0) {
  6634. do_log(-2,"ERROR sql_storage: too many retries ".
  6635. "on storing preliminary, info not saved");
  6636. } else {
  6637. do_log(2,"sql_storage: retrying prelim., $attempt attempts remain");
  6638. sleep(int(1+rand(3))); add_entropy(Time::HiRes::gettimeofday,$attempt);
  6639. }
  6640. }
  6641. };
  6642. section_time($which_section);
  6643. my($pbn) = c('policy_bank_path');
  6644. do_log(1,sprintf("Checking: %s %s%s%s -> %s", $mail_id,
  6645. $pbn eq '' ? '' : "$pbn ",
  6646. $cl_ip eq '' ? '' : "[$cl_ip] ",
  6647. qquote_rfc2821_local($msginfo->sender),
  6648. join(',', qquote_rfc2821_local(@recips)) ));
  6649. eval {
  6650. snmp_count('InMsgs');
  6651. snmp_count('InMsgsNullRPath') if $msginfo->sender eq '';
  6652. if (@recips == 1) { snmp_count( 'InMsgsRecips' ) }
  6653. elsif (@recips > 1) { snmp_count( ['InMsgsRecips',scalar(@recips)] ) }
  6654. # mkdir is a costly operation (must be atomic, flushes buffers).
  6655. # If we can re-use directory 'parts' from the previous invocation it saves
  6656. # us precious time. Together with matching rmdir this can amount to 10-15 %
  6657. # of total elapsed time! (no spam checking, depending on file system)
  6658. $which_section = "creating_partsdir";
  6659. my($errn) = lstat("$tempdir/parts") ? 0 : 0+$!;
  6660. if ($errn == ENOENT) { # needs to be created
  6661. mkdir("$tempdir/parts", 0750)
  6662. or die "Can't create directory $tempdir/parts: $!";
  6663. section_time('mkdir parts'); }
  6664. elsif ($errn != 0) { die "$tempdir/parts is not accessible: $!" }
  6665. elsif (!-d _) { die "$tempdir/parts is not a directory" }
  6666. else {} # fine, directory already exists
  6667. chdir($TEMPBASE) or die "Can't chdir to $TEMPBASE: $!";
  6668. # FIRST: what kind of e-mail did we get? call content scanners
  6669. # already in cache?
  6670. $which_section = "cached";
  6671. snmp_count('CacheAttempts');
  6672. my($cache_entry); my($now) = time;
  6673. my($cache_entry_ttl) =
  6674. max($virus_check_negative_ttl, $virus_check_positive_ttl,
  6675. $spam_check_negative_ttl, $spam_check_positive_ttl);
  6676. my($now_utc_iso8601) = iso8601_utc_timestamp($now,1);
  6677. my($expires_utc_iso8601) = iso8601_utc_timestamp($now+$cache_entry_ttl,1);
  6678. $cache_entry = $body_digest_cache->get($body_digest)
  6679. if $body_digest_cache && defined $body_digest;
  6680. if (!defined $cache_entry) {
  6681. snmp_count('CacheMisses');
  6682. $cache_entry->{'ctime'} = $now_utc_iso8601; # create a new cache record
  6683. } else {
  6684. snmp_count('CacheHits');
  6685. $virus_presence_checked = defined $cache_entry->{'VN'} ? 1 : 0;
  6686. # spam level and spam report may be influenced by mail header, not only
  6687. # by mail body, so caching based on body is only a close approximation;
  6688. # ignore spam cache if body is too small
  6689. $spam_presence_checked = defined $cache_entry->{'SL'} ? 1 : 0;
  6690. if ($msginfo->orig_body_size < 200) { $spam_presence_checked = 0 }
  6691. if ($virus_presence_checked && defined $cache_entry->{'Vt'}) {
  6692. # check for expiration of cached virus test results
  6693. my($ttl) = !@{$cache_entry->{'VN'}} ? $virus_check_negative_ttl
  6694. : $virus_check_positive_ttl;
  6695. if ($now > $cache_entry->{'Vt'} + $ttl) {
  6696. do_log(2,"Cached virus check expired, TTL = $ttl s");
  6697. $virus_presence_checked = 0;
  6698. }
  6699. }
  6700. if ($spam_presence_checked && defined $cache_entry->{'St'}) {
  6701. # check for expiration of cached spam test results
  6702. # (note: hard-wired spam level 6)
  6703. my($ttl) = $cache_entry->{'SL'} < 6 ? $spam_check_negative_ttl
  6704. : $spam_check_positive_ttl;
  6705. if ($now > $cache_entry->{'St'} + $ttl) {
  6706. do_log(2,"Cached spam check expired, TTL = $ttl s");
  6707. $spam_presence_checked = 0;
  6708. }
  6709. }
  6710. if ($virus_presence_checked) {
  6711. $av_output = $cache_entry->{'VO'};
  6712. @virusname = @{$cache_entry->{'VN'}};
  6713. @detecting_scanners = @{$cache_entry->{'VD'}};
  6714. $virus_dejavu = 1;
  6715. }
  6716. ($spam_level, $spam_status, $spam_report) = @$cache_entry{'SL','SS','SR'}
  6717. if $spam_presence_checked;
  6718. do_log(1,sprintf("cached %s from <%s> (%s,%s)",
  6719. $body_digest, $msginfo->sender,
  6720. $virus_presence_checked, $spam_presence_checked));
  6721. snmp_count('CacheHitsVirusCheck') if $virus_presence_checked;
  6722. snmp_count('CacheHitsVirusMsgs') if @virusname;
  6723. snmp_count('CacheHitsSpamCheck') if $spam_presence_checked;
  6724. snmp_count('CacheHitsSpamMsgs') if $spam_level >= 6; # a hack
  6725. ll(5) && do_log(5,sprintf("cache entry age: %s c=%s a=%s",
  6726. (@virusname ? 'V' : $spam_level > 5 ? 'S' : '.'),
  6727. $cache_entry->{'ctime'}, $cache_entry->{'atime'} ));
  6728. } # if defined $cache_entry
  6729. my($will_do_virus_scanning, $all_bypass_virus_checks);
  6730. if ($extra_code_antivirus) {
  6731. $all_bypass_virus_checks =
  6732. !grep {!lookup(0,$_, @{ca('bypass_virus_checks_maps')})} @recips;
  6733. $will_do_virus_scanning =
  6734. !$virus_presence_checked && !$all_bypass_virus_checks;
  6735. }
  6736. my($will_do_banned_checking) = # banned name checking will be needed?
  6737. @{ca('banned_filename_maps')} || cr('banned_namepath_re');
  6738. # will do decoding parts as deeply as possible? only if needed
  6739. my($will_do_parts_decoding) =
  6740. !c('bypass_decode_parts') &&
  6741. ($will_do_virus_scanning || $will_do_banned_checking);
  6742. $which_section = "mime_decode-1";
  6743. my($ent); ($ent,$mime_err) = mime_decode($fh, $tempdir, $parts_root);
  6744. $msginfo->mime_entity($ent);
  6745. prolong_timer($which_section);
  6746. if ($will_do_parts_decoding) { # decoding parts can take a lot of time
  6747. $which_section = "parts_decode_ext";
  6748. snmp_count('OpsDec');
  6749. ($hold,$any_undecipherable) =
  6750. Amavis::Unpackers::decompose_mail($tempdir,$file_generator_object);
  6751. }
  6752. if (grep {!lookup(0,$_,@{ca('bypass_header_checks_maps')})} @recips) {
  6753. push(@bad_headers, "MIME error: ".$mime_err)
  6754. if defined $mime_err && $mime_err ne '';
  6755. push(@bad_headers, check_header_validity($conn,$msginfo));
  6756. }
  6757. if ($will_do_banned_checking) { # check for banned file contents
  6758. $which_section = "check-banned";
  6759. check_for_banned_names($msginfo,$parts_root); # saves results in $msginfo
  6760. $banned_filename_any = 0; $banned_filename_all = 1;
  6761. for my $r (@{$msginfo->per_recip_data}) {
  6762. my($a) = $r->banned_parts;
  6763. if (!defined $a || !@$a) { $banned_filename_all = 0 }
  6764. else {
  6765. $banned_filename_any++;
  6766. my($rhs) = $r->banned_rhs;
  6767. if (defined $rhs) {
  6768. for my $j (0..$#{$a}) {
  6769. if ($rhs->[$j] =~ /^DISCARD/) {
  6770. $banned_dsn_suppress = 1;
  6771. do_log(4,sprintf('BANNED:%s: %s', $rhs->[$j],$rhs->[$j]));
  6772. }
  6773. }
  6774. }
  6775. }
  6776. }
  6777. ll(4) && do_log(4,sprintf("banned check: any=%d, all=%s (%d)",
  6778. $banned_filename_any, $banned_filename_all?'Y':'N',
  6779. scalar(@{$msginfo->per_recip_data})));
  6780. }
  6781. if ($virus_presence_checked) {
  6782. do_log(5, "virus_presence cached, skipping virus_scan");
  6783. } elsif (!$extra_code_antivirus) {
  6784. do_log(5, "no anti-virus code loaded, skipping virus_scan");
  6785. } elsif ($all_bypass_virus_checks) {
  6786. do_log(5, "bypassing of virus checks requested");
  6787. } elsif (defined $hold && $hold ne '') { # protect virus scanner from bombs
  6788. do_log(0, "NOTICE: Virus scanning skipped: $hold");
  6789. $will_do_virus_scanning = 0;
  6790. } else {
  6791. if (!$will_do_virus_scanning)
  6792. { do_log(-1, "NOTICE: will_do_virus_scanning is false???") }
  6793. if (!defined($msginfo->mime_entity)) {
  6794. $which_section = "mime_decode-3";
  6795. my($ent); ($ent,$mime_err) = mime_decode($fh, $tempdir, $parts_root);
  6796. $msginfo->mime_entity($ent);
  6797. prolong_timer($which_section);
  6798. }
  6799. # special case to make available a complete mail file for inspection
  6800. if ((defined($mime_err) && $mime_err ne '') ||
  6801. lookup(0,'MAIL',@keep_decoded_original_maps) ||
  6802. $any_undecipherable && lookup(0,'MAIL-UNDECIPHERABLE',
  6803. @keep_decoded_original_maps)) {
  6804. # keep the original email.txt by making a hard link to it in ./parts/
  6805. $which_section = "linking-to-MAIL";
  6806. my($newpart_obj) =
  6807. Amavis::Unpackers::Part->new("$tempdir/parts",$parts_root,1);
  6808. my($newpart) = $newpart_obj->full_name;
  6809. do_log(2, "providing full original message to scanners as $newpart".
  6810. (!$any_undecipherable ?'' :", $any_undecipherable undecipherable").
  6811. ($mime_err eq '' ? '' : ", MIME error: $mime_err") );
  6812. link($msginfo->mail_text_fn, $newpart)
  6813. or die sprintf("Can't create hard link %s to %s: %s",
  6814. $newpart, $msginfo->mail_text_fn, $!);
  6815. $newpart_obj->type_short('MAIL');
  6816. $newpart_obj->type_declared('message/rfc822');
  6817. }
  6818. $which_section = "virus_scan";
  6819. # some virus scanners behave badly if interrupted,
  6820. # so for now just turn off the timer
  6821. my($remaining_time) = alarm(0); # check time left, stop timer
  6822. my($av_ret);
  6823. eval {
  6824. my($vn, $ds);
  6825. ($av_ret, $av_output, $vn, $ds) =
  6826. Amavis::AV::virus_scan($tempdir, $child_task_count==1, $parts_root);
  6827. @virusname = @$vn; @detecting_scanners = @$ds; # copy
  6828. };
  6829. prolong_timer($which_section, $remaining_time); # restart timer
  6830. if ($@ ne '') {
  6831. chomp($@);
  6832. if ($@ eq "timed out") { # can't happen, timer is stopped
  6833. @virusname = (); $av_ret = 0; # assume not a virus!
  6834. do_log(-1, "virus_scan TIMED OUT, ASSUME NOT A VIRUS !!!");
  6835. } else {
  6836. $hold = "virus_scan: $@"; # request HOLD
  6837. $av_ret = 0; # pretend it was ok (msg should be held)
  6838. die "$hold\n"; # die, TEMPFAIL is preferred to HOLD
  6839. }
  6840. }
  6841. snmp_count('OpsVirusCheck');
  6842. defined($av_ret) or die "All virus scanners failed!";
  6843. @$cache_entry{'Vt','VO','VN','VD'} =
  6844. ($now, $av_output, \@virusname, \@detecting_scanners);
  6845. $virus_presence_checked = 1;
  6846. if (defined $snmp_db && @virusname) {
  6847. $which_section = "read_snmp_variables";
  6848. $virus_dejavu = 1
  6849. if !grep {!defined($_) || $_ == 0} # none with counter zero or undef
  6850. @{$snmp_db->read_snmp_variables(map {"virus.byname.$_"} @virusname)};
  6851. section_time($which_section);
  6852. }
  6853. }
  6854. $which_section = "post_virus_scan";
  6855. if ($virus_presence_checked) {
  6856. my($bpvcm) = ca('bypass_virus_checks_maps');
  6857. for my $r (@{$msginfo->per_recip_data}) {
  6858. $r->infected(lookup(0,$r->recip_addr,@$bpvcm) ? undef :
  6859. @virusname ? 1 : 0);
  6860. }
  6861. }
  6862. my($sender_contact,$sender_source);
  6863. if (!@virusname) { $sender_contact = $sender_source = $msginfo->sender }
  6864. else {
  6865. ($sender_contact,$sender_source) = best_try_originator(
  6866. $msginfo->sender, $msginfo->mime_entity, \@virusname);
  6867. section_time('best_try_originator');
  6868. }
  6869. $msginfo->sender_contact($sender_contact); # save it
  6870. $msginfo->sender_source($sender_source); # save it
  6871. # consider doing spam scanning
  6872. if (!$extra_code_antispam) {
  6873. do_log(5, "no anti-spam code loaded, skipping spam_scan");
  6874. } elsif (@virusname) {
  6875. do_log(5, "infected contents, skipping spam_scan");
  6876. } elsif ($banned_filename_all) {
  6877. do_log(5, "banned contents, skipping spam_scan");
  6878. } elsif (!grep {!lookup(0,$_,@{ca('bypass_spam_checks_maps')})} @recips) {
  6879. do_log(5, "bypassing of spam checks requested");
  6880. } else {
  6881. $which_section = "spam-wb-list";
  6882. my($any_wbl, $all_wbl) = Amavis::SpamControl::white_black_list(
  6883. $conn, $msginfo, $sql_wblist, $user_id_sql, $ldap_policy);
  6884. section_time($which_section);
  6885. if ($all_wbl) {
  6886. do_log(5, "sender white/blacklisted, skipping spam_scan");
  6887. } elsif ($spam_presence_checked) {
  6888. do_log(5, "spam_presence cached, skipping spam_scan");
  6889. } else {
  6890. $which_section = "spam_scan";
  6891. ($spam_level, $spam_status, $spam_report, $autolearn_status) =
  6892. Amavis::SpamControl::spam_scan($conn, $msginfo);
  6893. prolong_timer($which_section);
  6894. snmp_count('OpsSpamCheck');
  6895. @$cache_entry{'St','SL','SS','SR'} =
  6896. ($now, $spam_level, $spam_status, $spam_report);
  6897. $spam_presence_checked = 1;
  6898. }
  6899. }
  6900. # store to cache
  6901. $which_section = 'update_cache';
  6902. $cache_entry->{'atime'} = $now_utc_iso8601; # update accessed timestamp
  6903. $body_digest_cache->set($body_digest,$cache_entry,
  6904. $now_utc_iso8601,$expires_utc_iso8601)
  6905. if $body_digest_cache && defined $body_digest;
  6906. $cache_entry = undef; # discard the object, it is no longer needed
  6907. section_time($which_section);
  6908. snmp_count("virus.byname.$_") for @virusname;
  6909. # SECOND: now that we know what we got, decide what to do with it
  6910. $which_section = 'after_scanning';
  6911. my($considered_spam_by_some_recips,$considered_oversize_by_some_recips);
  6912. if (@virusname || $banned_filename_any) { # virus or banned filename found
  6913. # bad_headers do not enter this section, although code is ready for them;
  6914. # we'll handle bad headers later, if mail turns out not to be spam
  6915. $which_section = "deal_with_virus_or_banned";
  6916. for my $r (@{$msginfo->per_recip_data}) {
  6917. next if $r->recip_done; # already dealt with
  6918. my($final_destiny) = $r->infected ? c('final_virus_destiny')
  6919. : defined($r->banned_parts) && @{$r->banned_parts}
  6920. ? c('final_banned_destiny')
  6921. : @bad_headers ? c('final_bad_header_destiny')
  6922. : D_PASS;
  6923. my($whitelisted_for_malware) = 0;
  6924. # if ($final_destiny != D_PASS && lookup(0,$msginfo->sender,
  6925. # [new_RE(qr'bugtraq-return-.*@securityfocus\.com')] )) {
  6926. # $whitelisted_for_malware = 1;
  6927. # do_log(0, "malware accepted from sender ".$msginfo->sender);
  6928. # }
  6929. if ($final_destiny == D_PASS || $whitelisted_for_malware) {
  6930. # recipient wants this message, malicious or not
  6931. } elsif ((!$r->infected || # not a virus, ignored or we want it
  6932. lookup(0,$r->recip_addr, @{ca('virus_lovers_maps')})) &&
  6933. # not banned or we want it
  6934. (!defined($r->banned_parts) || !@{$r->banned_parts} ||
  6935. lookup(0,$r->recip_addr, @{ca('banned_files_lovers_maps')})) &&
  6936. (!@bad_headers || # not bad header or we want it
  6937. lookup(0,$r->recip_addr, @{ca('bad_header_lovers_maps')})) )
  6938. {
  6939. # clean, not noticed (bypass...), or recipient wants it
  6940. } else { # change mail destiny for those not wanting malware
  6941. $r->recip_destiny($final_destiny);
  6942. my($reason);
  6943. if ($r->infected)
  6944. { $reason = "VIRUS: " . join(", ", @virusname) }
  6945. elsif (defined($r->banned_parts) && @{$r->banned_parts})
  6946. { $reason = "BANNED: " . join(", ", @{$r->banned_parts}) }
  6947. elsif (@bad_headers)
  6948. { $reason = "BAD_HEADER: " . join(", ", @bad_headers) }
  6949. $reason = substr($reason,0,100)."..." if length($reason) > 100+3;
  6950. $r->recip_smtp_response( ($final_destiny == D_DISCARD
  6951. ? "250 2.7.1 Ok, discarded"
  6952. : "550 5.7.1 Message content rejected") .
  6953. ", id=$am_id - $reason");
  6954. $r->recip_done(1);
  6955. # note that 5xx status rejects may later be converted to bounces or
  6956. # discards, according to $*_destiny setting
  6957. }
  6958. }
  6959. $which_section = "virus_or_banned quar+notif";
  6960. ensure_mime_entity($msginfo, $fh, $tempdir, \@virusname, $parts_root);
  6961. # send notifications, quarantine it
  6962. do_virus($conn, $msginfo, $virus_dejavu);
  6963. } else { # perhaps some recips consider it spam?
  6964. # spaminess is an individual matter, we must compare spam level
  6965. # with each recipient setting, there is no single global criterium
  6966. # that the mail is spam
  6967. $which_section = "deal_with_spam";
  6968. my($final_destiny) = c('final_spam_destiny');
  6969. for my $r (@{$msginfo->per_recip_data}) {
  6970. next if $r->recip_done; # already dealt with
  6971. my($kill_level);
  6972. $kill_level = lookup(0,$r->recip_addr, @{ca('spam_kill_level_maps')});
  6973. my($boost) = $r->recip_score_boost;
  6974. $boost = 0 if !defined($boost); # avoid uninitialized value warning
  6975. my($should_be_killed) =
  6976. !$r->recip_whitelisted_sender &&
  6977. ($r->recip_blacklisted_sender ||
  6978. (defined $spam_level && defined $kill_level ?
  6979. $spam_level+$boost >= $kill_level : 0) );
  6980. next unless $should_be_killed;
  6981. # message is at or above kill level, or sender is blacklisted
  6982. $considered_spam_by_some_recips = 1;
  6983. if ($final_destiny == D_PASS ||
  6984. lookup(0,$r->recip_addr, @{ca('spam_lovers_maps')})) {
  6985. # do nothing, recipient wants this message, even if spam
  6986. } else { # change mail destiny for those not wanting spam
  6987. ll(3) && do_log(3,sprintf(
  6988. "SPAM-KILL, %s -> %s, score=%s, kill=%s%s",
  6989. qquote_rfc2821_local($msginfo->sender, $r->recip_addr),
  6990. (!defined $spam_level ? 'x'
  6991. : !defined $boost ? $spam_level
  6992. : $boost >= 0 ? $spam_level.'+'.$boost : $spam_level.$boost),
  6993. !defined $kill_level ? 'x' : 0+sprintf("%.3f",$kill_level),
  6994. $r->recip_blacklisted_sender ? ', BLACKLISTED' : ''));
  6995. $r->recip_destiny($final_destiny);
  6996. my($reason) =
  6997. $r->recip_blacklisted_sender ? 'sender blacklisted' : 'UBE';
  6998. $r->recip_smtp_response(($final_destiny == D_DISCARD
  6999. ? "250 2.7.1 Ok, discarded, $reason"
  7000. : "550 5.7.1 Message content rejected, $reason"
  7001. ) . ", id=$am_id");
  7002. $r->recip_done(1);
  7003. }
  7004. }
  7005. if ($considered_spam_by_some_recips) {
  7006. $which_section = "spam quar+notif";
  7007. ensure_mime_entity($msginfo, $fh, $tempdir, \@virusname, $parts_root);
  7008. do_spam($conn, $msginfo,
  7009. $spam_level, $spam_status, $spam_report, $autolearn_status);
  7010. section_time('post-do_spam');
  7011. }
  7012. }
  7013. if (@bad_headers) { # invalid mail headers
  7014. $which_section = "deal_with_bad_headers";
  7015. ensure_mime_entity($msginfo, $fh, $tempdir, \@virusname, $parts_root);
  7016. my($is_bulk) = $msginfo->mime_entity->head->get('precedence', 0);
  7017. chomp($is_bulk);
  7018. do_log(1,sprintf("BAD HEADER from %s<%s>: %s",
  7019. $is_bulk eq '' ? '' : "($is_bulk) ", $msginfo->sender,
  7020. $bad_headers[0]));
  7021. $is_bulk = $is_bulk=~/^(bulk|list|junk)/i ? $1 : undef;
  7022. my($any_badh); my($final_destiny) = c('final_bad_header_destiny');
  7023. for my $r (@{$msginfo->per_recip_data}) {
  7024. next if $r->recip_done; # already dealt with
  7025. if ($final_destiny == D_PASS ||
  7026. lookup(0,$r->recip_addr, @{ca('bad_header_lovers_maps')}))
  7027. {
  7028. # recipient wants this message, broken or not
  7029. } elsif ($final_destiny == D_BOUNCE &&
  7030. (defined $is_bulk || $msginfo->sender eq '')) {
  7031. # have mercy on mailing lists and DSN: since a bounce for such mail
  7032. # will be suppressed, it is probably better to just let a mail pass
  7033. } else { # change mail destiny for those not wanting it
  7034. $r->recip_destiny($final_destiny);
  7035. my($reason) = (split(/\n/, $bad_headers[0]))[0];
  7036. $r->recip_smtp_response(($final_destiny == D_DISCARD
  7037. ? "250 2.6.0 Ok, message with invalid header discarded"
  7038. : "554 5.6.0 Message with invalid header rejected"
  7039. ) . ", id=$am_id - $reason");
  7040. $r->recip_done(1);
  7041. $any_badh++;
  7042. }
  7043. }
  7044. if ($any_badh) { # we use the same code as for viruses or banned
  7045. # but only if it wasn't already handled as spam
  7046. do_virus($conn, $msginfo, 0); # send notifications, quarantine it
  7047. }
  7048. section_time($which_section);
  7049. }
  7050. my($mslm) = ca('message_size_limit_maps');
  7051. if (@$mslm) {
  7052. $which_section = "deal_with_mail_size";
  7053. my($mail_size) = $msginfo->msg_size;
  7054. for my $r (@{$msginfo->per_recip_data}) {
  7055. next if $r->recip_done; # already dealt with
  7056. my($size_limit) = lookup(0,$r->recip_addr, @$mslm);
  7057. $size_limit = 65536
  7058. if $size_limit && $size_limit < 65536; # rfc2821
  7059. if ($size_limit && $mail_size > $size_limit) {
  7060. do_log(1,sprintf("OVERSIZE from %s to %s: size %s B, limit %s B",
  7061. qquote_rfc2821_local($msginfo->sender),
  7062. qquote_rfc2821_local($r->recip_addr),
  7063. $mail_size, $size_limit))
  7064. if !$considered_oversize_by_some_recips;
  7065. $considered_oversize_by_some_recips = 1;
  7066. $r->recip_destiny(D_BOUNCE);
  7067. $r->recip_smtp_response("552 5.3.4 Message size ($mail_size B) ".
  7068. "exceeds recipient's size limit, id=$am_id");
  7069. $r->recip_done(1);
  7070. }
  7071. }
  7072. section_time($which_section);
  7073. }
  7074. $which_section = "aux_quarantine";
  7075. # do_quarantine($conn, $msginfo, undef,
  7076. # ['archive-files'], 'local:archive-ham/%m.gz'
  7077. # ) unless $considered_oversize_by_some_recips ||
  7078. # ref($msginfo->quarantined_to) && @{$msginfo->quarantined_to};
  7079. # do_quarantine($conn, $msginfo, undef,
  7080. # ['archive-files'], 'local:archive/%m');
  7081. # do_quarantine($conn, $msginfo, undef,
  7082. # ['archive@localhost'], 'local:all-%m');
  7083. # do_quarantine($conn, $msginfo, undef,
  7084. # ['sender-quarantine'], 'local:user-%m'
  7085. # ) if lookup(0,$msginfo->sender, ['user1@domain','user2@domain']);
  7086. # section_time($which_section);
  7087. $which_section = "checking_sender_ip";
  7088. my(@recips) = @{$msginfo->recips};
  7089. if ($considered_spam_by_some_recips && @recips==1 &&
  7090. $recips[0] eq $msginfo->sender &&
  7091. lookup(0,$msginfo->sender, @{ca('local_domains_maps')}))
  7092. { # ad-hoc check for externally originating spam with sender=recipient
  7093. # turns off spam bounce
  7094. my($cl_ip) = $msginfo->client_addr;
  7095. if ($cl_ip eq '') {
  7096. ensure_mime_entity($msginfo, $fh, $tempdir, \@virusname, $parts_root);
  7097. $cl_ip = fish_out_ip_from_received(
  7098. $msginfo->mime_entity->head->get('received',0));
  7099. }
  7100. if ($cl_ip ne '' && !lookup_ip_acl($cl_ip,@{ca('mynetworks_maps')})) {
  7101. do_log(2,"disabling DSN, spam from external source $cl_ip, ".
  7102. "local sender believed to be faked: ".$msginfo->sender);
  7103. $msginfo->sender_contact(undef); # believed to be faked
  7104. }
  7105. }
  7106. if (defined $hold && $hold ne '')
  7107. { do_log(-1, "NOTICE: HOLD reason: $hold") }
  7108. # THIRD: now that we know what to do with it, do it! (deliver or bounce)
  7109. my($which_content_counter) =
  7110. @virusname ? 'ContentVirusMsgs'
  7111. : $banned_filename_any ? 'ContentBannedMsgs'
  7112. : $considered_spam_by_some_recips ? 'ContentSpamMsgs'
  7113. : @bad_headers ? 'ContentBadHdrMsgs'
  7114. : $considered_oversize_by_some_recips ? 'ContentOversizeMsgs'
  7115. : 'ContentCleanMsgs';
  7116. snmp_count($which_content_counter);
  7117. my($hdr_edits) = $msginfo->header_edits;
  7118. if (!$hdr_edits) {
  7119. $hdr_edits = Amavis::Out::EditHeader->new;
  7120. $msginfo->header_edits($hdr_edits);
  7121. }
  7122. if ($msginfo->delivery_method eq '') { # AM.PDP or AM.CL (milter)
  7123. $which_section = "AM.PDP headers";
  7124. ensure_mime_entity($msginfo, $fh, $tempdir, \@virusname, $parts_root);
  7125. $hdr_edits = add_forwarding_header_edits_common(
  7126. $conn, $msginfo, $hdr_edits, $hold, $any_undecipherable,
  7127. $virus_presence_checked, $spam_presence_checked,
  7128. $spam_level, $spam_status, $spam_report, $autolearn_status,
  7129. undef);
  7130. my($done_all);
  7131. my($recip_cl); # ref to a list of similar recip objects
  7132. ($hdr_edits, $recip_cl, $done_all) =
  7133. add_forwarding_header_edits_per_recip(
  7134. $conn, $msginfo, $hdr_edits, $hold, $any_undecipherable,
  7135. $virus_presence_checked, $spam_presence_checked,
  7136. $spam_level, $spam_status, $spam_report, $autolearn_status,
  7137. undef, undef);
  7138. $msginfo->header_edits($hdr_edits); # store edits (redundant?)
  7139. if (@$recip_cl && !$done_all) {
  7140. do_log(-1, "AM.PDP: CLIENTS REQUIRE DIFFERENT HEADERS");
  7141. };
  7142. } elsif (grep { !$_->recip_done } @{$msginfo->per_recip_data}) { # forward
  7143. # To be delivered explicitly - only to those recipients not yet marked
  7144. # as 'done' by the above content filtering sections.
  7145. $which_section = "forwarding";
  7146. ensure_mime_entity($msginfo, $fh, $tempdir, \@virusname, $parts_root);
  7147. # a quick-fix solution to defang dangerous contents
  7148. my($mail_defanged); # nonempty indicates mail body is replaced
  7149. my($explanation); my($defang_all) = c('defang_all');
  7150. if ($hold ne '') { $explanation =
  7151. "WARNING: possible mail bomb, NOT CHECKED FOR VIRUSES:\n $hold";
  7152. } elsif (@virusname) {
  7153. $explanation = 'WARNING: contains virus '.join(' ',@virusname)
  7154. if c('defang_virus') || $defang_all;
  7155. } elsif ($banned_filename_any) {
  7156. $explanation = "WARNING: contains banned part"
  7157. if c('defang_banned') || $defang_all;
  7158. } elsif ($any_undecipherable) {
  7159. $explanation = "WARNING: contains undecipherable part"
  7160. if c('defang_undecipherable') || $defang_all;
  7161. } elsif ($considered_spam_by_some_recips) {
  7162. $explanation = $spam_report
  7163. if c('defang_spam') || $defang_all;
  7164. } elsif (@bad_headers) {
  7165. $explanation = 'WARNING: bad headers '.join(' ',@bad_headers)
  7166. if c('defang_bad_header') || $defang_all;
  7167. } else { $explanation = '(clean)' if $defang_all }
  7168. if (defined $explanation) { # malware
  7169. $explanation .= "\n" if $explanation !~ /\n\z/;
  7170. my($s) = $explanation; $s=~s/[ \t\n]+\z//;
  7171. if (length($s) > 100) { $s = substr($s,0,100-3) . "..." }
  7172. do_log(1, "DEFANGING MAIL: $s");
  7173. my($d) = defanged_mime_entity($conn,$msginfo,$explanation);
  7174. $msginfo->mail_text($d); # substitute mail with rewritten version
  7175. $msginfo->mail_text_fn(undef); # remove filename information
  7176. $mail_defanged = 'Original mail wrapped as attachment (defanged)';
  7177. section_time('defang');
  7178. }
  7179. $hdr_edits = add_forwarding_header_edits_common(
  7180. $conn, $msginfo, $hdr_edits, $hold, $any_undecipherable,
  7181. $virus_presence_checked, $spam_presence_checked,
  7182. $spam_level, $spam_status, $spam_report, $autolearn_status,
  7183. $mail_defanged);
  7184. for (;;) { # do the delivery
  7185. my($r_hdr_edits) = Amavis::Out::EditHeader->new; # per-recip edits set
  7186. $r_hdr_edits->inherit_header_edits($hdr_edits);
  7187. my($done_all);
  7188. my($recip_cl); # ref to a list of similar recip objects
  7189. ($r_hdr_edits, $recip_cl, $done_all) =
  7190. add_forwarding_header_edits_per_recip(
  7191. $conn, $msginfo, $r_hdr_edits, $hold, $any_undecipherable,
  7192. $virus_presence_checked, $spam_presence_checked,
  7193. $spam_level, $spam_status, $spam_report, $autolearn_status,
  7194. $mail_defanged, undef);
  7195. last if !@$recip_cl;
  7196. $msginfo->header_edits($r_hdr_edits); # store edits
  7197. mail_dispatch($conn, $msginfo, 0, $dsn_per_recip_capable,
  7198. sub { my($r) = @_; grep { $_ eq $r } @$recip_cl });
  7199. snmp_count('OutForwMsgs');
  7200. snmp_count('OutForwHoldMsgs') if $hold ne '';
  7201. $point_of_no_return = 1; # now past the point where mail was sent
  7202. last if $done_all;
  7203. }
  7204. }
  7205. prolong_timer($which_section);
  7206. $which_section = "delivery-notification";
  7207. my($dsn_needed); my($warnsender_with_pass,$which_dsn_counter,$dsnmsgref);
  7208. ($smtp_resp, $exit_code, $dsn_needed) =
  7209. one_response_for_all($msginfo, $dsn_per_recip_capable, $am_id);
  7210. if ($smtp_resp =~ /^2/ && !$dsn_needed) {
  7211. ($warnsender_with_pass,$which_dsn_counter,$dsnmsgref) =
  7212. @virusname && c('warnvirussender') ?
  7213. (1, 'OutDsnVirusMsgs', cr('notify_virus_sender_templ'))
  7214. : $banned_filename_any && c('warnbannedsender') ?
  7215. (1, 'OutDsnBannedMsgs', cr('notify_virus_sender_templ'))
  7216. : $considered_spam_by_some_recips && c('warnspamsender') ?
  7217. (1, 'OutDsnSpamMsgs', cr('notify_spam_sender_templ'))
  7218. : @bad_headers && c('warnbadhsender') ?
  7219. (1, 'OutDsnBadHdrMsgs', cr('notify_sender_templ')) : (0,undef,undef);
  7220. }
  7221. ll(4) && do_log(4,sprintf(
  7222. "warnsender_with_pass=%s (%s,%s,%s,%s), ".
  7223. "dsn_needed=%s, cnt=%s, exit=%s, %s",
  7224. map {defined $_ ? $_ : ''} ( # avoid warnings about uninitialized value
  7225. $warnsender_with_pass,
  7226. c('warnvirussender'),c('warnbannedsender'),
  7227. c('warnbadhsender'),c('warnspamsender'),
  7228. $dsn_needed,$which_dsn_counter,$exit_code,$smtp_resp) ));
  7229. if ($dsn_needed || $warnsender_with_pass) {
  7230. ensure_mime_entity($msginfo, $fh, $tempdir, \@virusname, $parts_root);
  7231. my($what_bad_content) = join(' & ',
  7232. !@virusname ? () : 'VIRUS',
  7233. !$banned_filename_any ? () : 'BANNED',
  7234. !$considered_spam_by_some_recips ? () : 'SPAM',
  7235. !@bad_headers ? () : 'BAD HEADER',
  7236. !$considered_oversize_by_some_recips ? () : 'OVERSIZE');
  7237. my($notification); my($dsn_cutoff_level);
  7238. if ($msginfo->sender eq '') { # don't respond to null reverse path
  7239. my($msg) = "DSN contains $what_bad_content; bounce is not bouncible";
  7240. if (!$dsn_needed) { do_log(4, $msg) }
  7241. else { do_log(1, "NOTICE: $msg, mail intentionally dropped") }
  7242. $msginfo->dsn_sent(2); # pretend the message was bounced
  7243. } elsif ($msginfo->sender_contact eq '') {
  7244. my($msg) = sprintf("Not sending DSN to believed-to-be-faked "
  7245. . "sender <%s>, mail containing %s",
  7246. $msginfo->sender, $what_bad_content);
  7247. if (!$dsn_needed) { do_log(4, $msg) }
  7248. else { do_log(2, "NOTICE: $msg intentionally dropped") }
  7249. $msginfo->dsn_sent(2); # pretend the message was bounced
  7250. } elsif ($banned_dsn_suppress) {
  7251. my($msg) = "Not sending DSN, as suggested by banned rule";
  7252. if (!$dsn_needed) { do_log(4, $msg) }
  7253. else { do_log(1, "NOTICE: $msg, mail intentionally dropped") }
  7254. $msginfo->dsn_sent(2); # pretend the message was bounced
  7255. } elsif (defined $spam_level &&
  7256. !grep { $dsn_cutoff_level = lookup(0,$_->recip_addr,
  7257. @{ca('spam_dsn_cutoff_level_maps')}),
  7258. !defined($dsn_cutoff_level) ||
  7259. $spam_level + $_->recip_score_boost < $dsn_cutoff_level
  7260. } @{$msginfo->per_recip_data} ) {
  7261. my($msg) = "Not sending DSN, spam level exceeds DSN cutoff level for all recips";
  7262. if (!$dsn_needed) { do_log(4, $msg) }
  7263. else { do_log(1, "NOTICE: $msg, mail intentionally dropped") }
  7264. $msginfo->dsn_sent(2); # pretend the message was bounced
  7265. } elsif ((@virusname || $banned_filename_any ||
  7266. $considered_spam_by_some_recips || @bad_headers ||
  7267. $considered_oversize_by_some_recips) &&
  7268. $msginfo->mime_entity->head->get('precedence',0)
  7269. =~ /^(bulk|list|junk)/i )
  7270. { my($msg) = sprintf("Not sending DSN in response to bulk mail "
  7271. . "from <%s> containing %s",
  7272. $msginfo->sender, $what_bad_content);
  7273. if (!$dsn_needed) { do_log(4, $msg) }
  7274. else { do_log(1, "NOTICE: $msg, mail intentionally dropped") }
  7275. $msginfo->dsn_sent(2); # pretend the message was bounced
  7276. } else { # prepare a notification
  7277. ### TODO: better selection of DSN reason is still needed!
  7278. if (!$warnsender_with_pass) { # it will be a non-delivery notification
  7279. my($prio) = 0; # choose the most relevant notification template
  7280. for my $r (@{$msginfo->per_recip_data}) {
  7281. local($_) = $r->recip_done ? $r->recip_smtp_response : $smtp_resp;
  7282. my($t_prio,$t_which_dsn_counter,$t_dsnmsgref) =
  7283. /^([25]).*\bVIRUS\b/ ?
  7284. ($1*10+5, 'OutDsnVirusMsgs', cr('notify_virus_sender_templ'))
  7285. : /^([25]).*\bBANNED\b/ ?
  7286. ($1*10+4, 'OutDsnBannedMsgs',cr('notify_virus_sender_templ'))
  7287. : /^([25]).*\b(?:UBE|blacklisted)\b/ ?
  7288. ($1*10+3, 'OutDsnSpamMsgs', cr('notify_spam_sender_templ'))
  7289. : /^([25]).*\bheader\b/ ?
  7290. ($1*10+2, 'OutDsnBadHdrMsgs',cr('notify_sender_templ'))
  7291. : (0, undef, undef);
  7292. ($prio,$which_dsn_counter,$dsnmsgref) =
  7293. ($t_prio,$t_which_dsn_counter,$t_dsnmsgref) if $t_prio > $prio;
  7294. }
  7295. }
  7296. ($which_dsn_counter,$dsnmsgref) =
  7297. ('OutDsnOtherMsgs',cr('notify_sender_templ')) if !defined $dsnmsgref;
  7298. do_log(4,"notification chosen: $which_dsn_counter, $dsnmsgref");
  7299. # generate delivery status notification according to rfc3462 & rfc3464
  7300. $notification = delivery_status_notification($conn, $msginfo,
  7301. $warnsender_with_pass, \%builtins, $dsnmsgref) if $dsnmsgref;
  7302. snmp_count($which_dsn_counter) if defined $notification;
  7303. }
  7304. if (defined $notification) { # dsn needed, send delivery notification
  7305. mail_dispatch($conn, $notification, 1, 0);
  7306. snmp_count('OutDsnMsgs');
  7307. my($n_smtp_resp, $n_exit_code, $n_dsn_needed) =
  7308. one_response_for_all($notification, 0, $am_id); # check status
  7309. if ($n_smtp_resp =~ /^2/ && !$n_dsn_needed) { # dsn successful?
  7310. $msginfo->dsn_sent(1); # mark the message as bounced
  7311. $point_of_no_return = 2; # now past the point where DSN was sent
  7312. } elsif ($n_smtp_resp =~ /^4/) {
  7313. snmp_count('OutDsnTempFails');
  7314. die sprintf("temporarily unable to send DSN to <%s>: %s",
  7315. $msginfo->sender_contact, $n_smtp_resp);
  7316. } else {
  7317. snmp_count('OutDsnRejects');
  7318. do_log(-1,sprintf("NOTICE: UNABLE TO SEND DSN to <%s>: %s",
  7319. $msginfo->sender, $n_smtp_resp));
  7320. # # if dsn can not be sent, try to send it to postmaster
  7321. # $notification->recips(['postmaster']);
  7322. # # attempt double bounce
  7323. # mail_dispatch($conn, $notification, 1, 0);
  7324. }
  7325. # $notification->purge;
  7326. }
  7327. }
  7328. prolong_timer($which_section);
  7329. # generate customized log report at log level 0 - this is usually the
  7330. # only log entry interesting to administrators during normal operation
  7331. $which_section = 'main_log_entry';
  7332. my(%mybuiltins) = %builtins; # make a local copy
  7333. { # do a per-message log entry
  7334. my($s) = $spam_status;
  7335. $s =~ s/^tests=\[ ( [^\]]* ) \]/$1/x; my(@s) = split(/,/,$s);
  7336. if (@s > 50) { $#s = 50-1; push(@s,"...") } # arbitrary sanity limit
  7337. $mybuiltins{'T'} = \@s; # macro %T has overloaded semantics, ugly
  7338. my($y,$n,$f) = delivery_short_report($msginfo);
  7339. @mybuiltins{'D','O','N'} = ($y,$n,$f);
  7340. my($strr) = expand(cr('log_templ'), \%mybuiltins);
  7341. for my $logline (split(/[ \t]*\n/, $$strr)) {
  7342. do_log(0, $logline) if $logline ne '';
  7343. }
  7344. }
  7345. if (c('log_recip_templ') ne '') { # do per-recipient log entries
  7346. # redefine macros with a by-recipient semantics
  7347. for my $r (@{$msginfo->per_recip_data}) {
  7348. # recipient counter in macro %. may indicate to the template
  7349. # that a per-recipient expansion semantics is expected
  7350. $mybuiltins{'.'}++;
  7351. my($recip) = $r->recip_addr;
  7352. my($smtp_resp) = $r->recip_smtp_response;
  7353. my($qrecip_addr) = scalar(qquote_rfc2821_local($recip));
  7354. $mybuiltins{'D'} = $mybuiltins{'O'} = $mybuiltins{'N'} = undef;
  7355. if ($r->recip_destiny==D_PASS && ($smtp_resp=~/^2/ || !$r->recip_done)){
  7356. $mybuiltins{'D'} = $qrecip_addr;
  7357. } else {
  7358. $mybuiltins{'O'} = $qrecip_addr;
  7359. my($remote_mta) = $r->recip_remote_mta;
  7360. $mybuiltins{'N'} = sprintf("%s:%s\n %s", $qrecip_addr,
  7361. ($remote_mta eq '' ? '' : " $remote_mta said:"), $smtp_resp);
  7362. }
  7363. my(@b); @b = @{$r->banned_parts} if defined $r->banned_parts;
  7364. my($b_chopped) = @b > 2; @b = (@b[0,1],'...') if $b_chopped;
  7365. s/[ \t]{6,}/ ... /g for @b;
  7366. $mybuiltins{'F'} = \@b; # list of banned file names
  7367. my($blacklisted) = $r->recip_blacklisted_sender;
  7368. my($whitelisted) = $r->recip_whitelisted_sender;
  7369. my($boost) = $r->recip_score_boost;
  7370. my($is_local,$tag_level,$tag2_level,$kill_level);
  7371. $is_local = lookup(0,$recip, @{ca('local_domains_maps')});
  7372. $tag_level = lookup(0,$recip, @{ca('spam_tag_level_maps')});
  7373. $tag2_level = lookup(0,$recip, @{ca('spam_tag2_level_maps')});
  7374. $kill_level = lookup(0,$recip, @{ca('spam_kill_level_maps')});
  7375. my($do_tag) =
  7376. $blacklisted || !defined $tag_level ||
  7377. (defined $spam_level ? $spam_level+$boost >= $tag_level
  7378. : $whitelisted ? (-10 >= $tag_level) : 0);
  7379. my($do_tag2) = !$whitelisted &&
  7380. ( $blacklisted ||
  7381. (defined $spam_level && defined $tag2_level ?
  7382. $spam_level+$boost >= $tag2_level : 0) );
  7383. my($do_kill) = !$whitelisted &&
  7384. ( $blacklisted ||
  7385. (defined $spam_level && defined $kill_level ?
  7386. $spam_level+$boost >= $kill_level : 0) );
  7387. for ($do_tag,$do_tag2,$do_kill) { $_ = $_ ? 'Y' : '0' } # normalize
  7388. for ($is_local) { $_ = $_ ? 'L' : '0' } # normalize
  7389. for ($tag_level,$tag2_level,$kill_level) { $_ = 'x' if !defined($_) }
  7390. $mybuiltins{'R'} = $recip;
  7391. $mybuiltins{'c'} = do { # format SA score +/- by-sender score boost
  7392. if (!defined($spam_level)) { '-' }
  7393. else {
  7394. my($sl) = 0+sprintf("%.3f",$spam_level); # trim down fraction
  7395. my($b) = !defined $boost ? undef : 0+sprintf("%.3f",$boost);
  7396. !defined $boost || $boost == 0 ? $sl
  7397. : $boost >= 0 ? $sl.'+'.$b : $sl.$b;
  7398. }
  7399. };
  7400. @mybuiltins{('0','1','2','k')} = ($is_local,$do_tag,$do_tag2,$do_kill);
  7401. # macros %3, %4, %5 are experimental, until a better solution is found
  7402. @mybuiltins{('3','4','5')} = ($tag_level,$tag2_level,$kill_level);
  7403. my($strr) = expand(cr('log_recip_templ'), \%mybuiltins);
  7404. for my $logline (split(/[ \t]*\n/, $$strr)) {
  7405. do_log(0, $logline) if $logline ne '';
  7406. }
  7407. }
  7408. }
  7409. section_time($which_section);
  7410. if ($sql_storage) { # save final information to SQL (if enabled)
  7411. $which_section = 'sql-update';
  7412. my($ds) = $msginfo->dsn_sent;
  7413. $ds = !$ds ? 'N' : $ds==1 ? 'Y' : $ds==2 ? 'q' : '?';
  7414. my($ct) = @virusname ? 'V' : $banned_filename_any ? 'B' :
  7415. $considered_spam_by_some_recips ? 'S' : @bad_headers ? 'H' :
  7416. $considered_oversize_by_some_recips ? 'O' : 'C';
  7417. for (my($attempt)=5; $attempt>0; ) { # sanity limit on retries
  7418. $sql_storage->save_info_final($conn,$msginfo,$spam_level,$ds,$ct)
  7419. and last;
  7420. if (--$attempt <= 0) {
  7421. do_log(-2,"ERROR sql_storage: too many retries ".
  7422. "on storing final, info not saved");
  7423. } else {
  7424. do_log(2,"sql_storage: retrying on final, $attempt attempts remain");
  7425. sleep(int(1+rand(3))); # can't mix Time::HiRes::sleep with alarm
  7426. }
  7427. };
  7428. section_time($which_section);
  7429. }
  7430. if (defined $snmp_db) {
  7431. $which_section = 'update_snmp';
  7432. snmp_count( ['entropy',0,'STR'] );
  7433. $snmp_db->update_snmp_variables;
  7434. section_time($which_section);
  7435. }
  7436. $which_section = 'finishing';
  7437. }; # end eval
  7438. if ($@ ne '') {
  7439. chomp($@);
  7440. $preserve_evidence = 1;
  7441. my($msg) = "$which_section FAILED: $@";
  7442. if ($point_of_no_return) {
  7443. do_log(-2, "TROUBLE in check_mail, ".
  7444. "but must continue ($point_of_no_return): $msg");
  7445. } else {
  7446. do_log(-2, "TROUBLE in check_mail: $msg");
  7447. $smtp_resp = "451 4.5.0 Error in processing, id=$am_id, $msg";
  7448. $exit_code = EX_TEMPFAIL;
  7449. for my $r (@{$msginfo->per_recip_data})
  7450. { $r->recip_smtp_response($smtp_resp); $r->recip_done(1) }
  7451. }
  7452. }
  7453. # if ($hold ne '') {
  7454. # do_log(-1, "NOTICE: Evidence is to be preserved: $hold");
  7455. # $preserve_evidence = 1;
  7456. # }
  7457. if (!$preserve_evidence && debug_oneshot()) {
  7458. do_log(0, "DEBUG_ONESHOT CAUSES EVIDENCE TO BE PRESERVED");
  7459. $preserve_evidence = 1;
  7460. }
  7461. my($which_counter) = 'InUnknown';
  7462. if ($smtp_resp =~ /^4/) { $which_counter = 'InTempFails' }
  7463. elsif ($smtp_resp =~ /^5/) { $which_counter = 'InRejects' }
  7464. elsif ($smtp_resp =~ /^2/) {
  7465. my($dsn_sent) = $msginfo->dsn_sent;
  7466. if (!$dsn_sent) { $which_counter = $msginfo->delivery_method ne ''
  7467. ? 'InAccepts' : 'InContinues' }
  7468. elsif ($dsn_sent==1) { $which_counter = 'InBounces' }
  7469. elsif ($dsn_sent==2) { $which_counter = 'InDiscards' }
  7470. }
  7471. snmp_count($which_counter);
  7472. $snmp_db->register_proc('.') if defined $snmp_db; # content checking done
  7473. $MSGINFO = undef; # release global reference to msginfo object
  7474. ($smtp_resp, $exit_code, $preserve_evidence);
  7475. }
  7476. # Ensure we have $msginfo->$entity defined when we expect we'll need it,
  7477. # e.g. to construct notifications. While at it, also get us some additional
  7478. # information on sender from the header.
  7479. #
  7480. sub ensure_mime_entity($$$$$) {
  7481. my($msginfo, $fh, $tempdir, $virusname_list, $parts_root) = @_;
  7482. if (!defined($msginfo->mime_entity)) {
  7483. # header may not have been parsed yet, e.g. if the result was cached
  7484. my($ent,$mime_err) = mime_decode($fh, $tempdir, $parts_root);
  7485. $msginfo->mime_entity($ent);
  7486. prolong_timer("ensure_mime_entity");
  7487. }
  7488. }
  7489. sub add_forwarding_header_edits_common($$$$$$$$$$$$) {
  7490. my($conn, $msginfo, $hdr_edits, $hold, $any_undecipherable,
  7491. $virus_presence_checked, $spam_presence_checked,
  7492. $spam_level, $spam_status, $spam_report, $autolearn_status,
  7493. $mail_defanged) = @_;
  7494. $hdr_edits->prepend_header('Received',
  7495. received_line($conn,$msginfo,am_id(),1), 1)
  7496. if $insert_received_line && $msginfo->delivery_method ne '';
  7497. # discard existing X-Amavis-Hold header field, only allow our own
  7498. $hdr_edits->delete_header('X-Amavis-Hold');
  7499. if ($hold ne '') {
  7500. $hdr_edits->append_header('X-Amavis-Hold', $hold);
  7501. do_log(-1, "Inserting header field: X-Amavis-Hold: $hold");
  7502. }
  7503. if ($mail_defanged ne '') {
  7504. # prepend Resent-* header fields, they must precede
  7505. # corresponding Received header field (pushed in reverse order)
  7506. $hdr_edits->prepend_header('Resent-Message-ID',
  7507. sprintf('<RE%s@%s>',$msginfo->mail_id,$myhostname) );
  7508. $hdr_edits->prepend_header('Resent-Date',
  7509. rfc2822_timestamp($msginfo->rx_time));
  7510. $hdr_edits->prepend_header('Resent-From', c('hdrfrom_notify_recip'));
  7511. # append X-Amavis-Modified
  7512. my($msg) = "$mail_defanged by $myhostname";
  7513. $hdr_edits->append_header('X-Amavis-Modified', $msg);
  7514. do_log(1, "Inserting header field: X-Amavis-Modified: $msg");
  7515. }
  7516. if ($extra_code_antivirus) {
  7517. $hdr_edits->delete_header('X-Amavis-Alert');
  7518. $hdr_edits->delete_header(c('X_HEADER_TAG'))
  7519. if c('remove_existing_x_scanned_headers') &&
  7520. (c('X_HEADER_LINE') ne '' && c('X_HEADER_TAG') =~ /^[!-9;-\176]+\z/);
  7521. }
  7522. if ($extra_code_antispam) {
  7523. if (c('remove_existing_spam_headers')) {
  7524. my(@which_headers) = qw(
  7525. X-Spam-Status X-Spam-Level X-Spam-Flag X-Spam-Score
  7526. X-Spam-Report X-Spam-Checker-Version X-Spam-Tests);
  7527. push(@which_headers, qw(
  7528. X-DSPAM-Result X-DSPAM-Confidence X-DSPAM-Probability
  7529. X-DSPAM-Signature X-DSPAM-User X-DSPAM-Factors)) if defined $dspam;
  7530. for my $h (@which_headers) { $hdr_edits->delete_header($h) }
  7531. }
  7532. # $hdr_edits->append_header('X-Spam-Checker-Version',
  7533. # sprintf("SpamAssassin %s (%s) on %s", Mail::SpamAssassin::Version(),
  7534. # $Mail::SpamAssassin::SUB_VERSION, $myhostname));
  7535. }
  7536. $hdr_edits;
  7537. }
  7538. # Prepare header edits for the first not-yet-done recipient.
  7539. # Inspect remaining recipients, returning the list of recipient objects
  7540. # that are receiving the same set of header edits (so the message may be
  7541. # delivered to them in one SMTP transaction).
  7542. #
  7543. sub add_forwarding_header_edits_per_recip($$$$$$$$$$$$$) {
  7544. my($conn, $msginfo, $hdr_edits, $hold, $any_undecipherable,
  7545. $virus_presence_checked, $spam_presence_checked,
  7546. $spam_level, $spam_status, $spam_report, $autolearn_status,
  7547. $mail_defanged, $filter) = @_;
  7548. my(@recip_cluster);
  7549. my(@per_recip_data) = grep { !$_->recip_done && (!$filter || &$filter($_)) }
  7550. @{$msginfo->per_recip_data};
  7551. my($per_recip_data_len) = scalar(@per_recip_data);
  7552. my($first) = 1; my($cluster_key); my($cluster_full_spam_status);
  7553. for my $r (@per_recip_data) {
  7554. my($recip) = $r->recip_addr;
  7555. my($is_local,$blacklisted,$whitelisted,$boost,$tag_level,$tag2_level,
  7556. $do_tag_virus_checked,$do_tag_virus,$do_tag_banned,$do_tag_badh,
  7557. $do_tag,$do_tag2,$do_subj,$do_subj_u,$subject_tag,$subject_tag2,$bypassed);
  7558. $is_local = lookup(0,$recip, @{ca('local_domains_maps')});
  7559. $do_tag_badh = @bad_headers &&
  7560. !lookup(0,$recip,@{ca('bypass_header_checks_maps')});
  7561. $do_tag_banned= defined($r->banned_parts) && @{$r->banned_parts};
  7562. $do_tag_virus = $r->infected; # 1, 0, or undef
  7563. $do_tag_virus_checked = defined($do_tag_virus) &&
  7564. (c('X_HEADER_LINE') ne '' && c('X_HEADER_TAG') =~ /^[!-9;-\176]+\z/);
  7565. if ($extra_code_antispam) {
  7566. # my($bypassed);
  7567. $blacklisted = $r->recip_blacklisted_sender;
  7568. $whitelisted = $r->recip_whitelisted_sender;
  7569. $boost = $r->recip_score_boost;
  7570. $bypassed = lookup(0,$recip, @{ca('bypass_spam_checks_maps')});
  7571. $tag_level = lookup(0,$recip, @{ca('spam_tag_level_maps')});
  7572. $tag2_level = lookup(0,$recip, @{ca('spam_tag2_level_maps')});
  7573. # spam-related headers should _not_ be inserted for:
  7574. # - nonlocal recipients (outgoing mail), as a matter of courtesy
  7575. # to our users;
  7576. # - recipients matching bypass_spam_checks: even though spam checking
  7577. # may have been done for other reasons, these recipients do not
  7578. # expect such headers, so let's pretend the check has not been done
  7579. # and not insert spam-related headers for them
  7580. $do_tag = $is_local && !$bypassed &&
  7581. ( $blacklisted || !defined $tag_level ||
  7582. (defined $spam_level ? $spam_level+$boost >= $tag_level
  7583. : $whitelisted ? (-10 >= $tag_level) : 0) );
  7584. $do_tag2 = $is_local && !$bypassed && !$whitelisted &&
  7585. ( $blacklisted ||
  7586. (defined $spam_level && defined $tag2_level ?
  7587. $spam_level+$boost >= $tag2_level : 0) );
  7588. $subject_tag2 = !$do_tag2 ? undef
  7589. : lookup(0,$recip, @{ca('spam_subject_tag2_maps')});
  7590. $subject_tag = !($do_tag||$do_tag2) ? undef
  7591. : lookup(0,$recip, @{ca('spam_subject_tag_maps')});
  7592. $do_subj = ($subject_tag2 ne '' || $subject_tag ne '') &&
  7593. lookup(0,$recip, @{ca('spam_modifies_subj_maps')});
  7594. }
  7595. if ($hold ne '' || $any_undecipherable) { # adding *UNCHECKED* subject tag?
  7596. $do_subj_u = $is_local && !$r->infected &&
  7597. c('undecipherable_subject_tag') ne '';
  7598. }
  7599. # normalize
  7600. for ($do_tag_virus_checked, $do_tag_virus, $do_tag_banned, $do_tag_badh,
  7601. $do_tag, $do_tag2, $do_subj, $do_subj_u, $is_local) { $_ = $_?1:0 }
  7602. my($spam_level_bar, $full_spam_status);
  7603. if ($do_tag || $do_tag2) {
  7604. my($slc) = c('sa_spam_level_char');
  7605. $spam_level_bar =
  7606. $slc x min($blacklisted ? 64 : $spam_level+$boost, 64) if $slc ne '';
  7607. my($s) = $spam_status; $s =~ s/,/,\n /g; # allow header field wrapping
  7608. $full_spam_status = sprintf("%s,\n score=%s\n%s%s %s%s",
  7609. ($do_tag2 || $do_tag) ? 'Yes' : 'No', #added by awi to get spamflag for yellow
  7610. !defined $spam_level ? 'x' : 0+sprintf("%.3f",$spam_level+$boost),
  7611. !defined $tag_level ? '' : sprintf(" tagged_above=%s\n",$tag_level),
  7612. !defined $tag2_level ? '' : sprintf(" required=%s\n", $tag2_level),
  7613. join('', $blacklisted ? "BLACKLISTED\n " : (),
  7614. $whitelisted ? "WHITELISTED\n " : ()),
  7615. $s);
  7616. } elsif (!$bypassed) {
  7617. my($slc) = c('sa_spam_level_char');
  7618. $spam_level_bar =
  7619. $slc x min($blacklisted ? 64 : $spam_level+$boost, 64) if $slc ne '';
  7620. my($s) = $spam_status; $s =~ s/,/,\n /g; # allow header field wrapping
  7621. $full_spam_status = sprintf("%s,\n score=%s\n%s%s %s%s",
  7622. ($do_tag2 || $do_tag) ? 'Yes' : 'No', #added by awi to get spamflag for yellow
  7623. !defined $spam_level ? 'x' : 0+sprintf("%.3f",$spam_level+$boost),
  7624. !defined $tag_level ? '' : sprintf(" tagged_above=%s\n",$tag_level),
  7625. !defined $tag2_level ? '' : sprintf(" required=%s\n", $tag2_level),
  7626. join('', $blacklisted ? "BLACKLISTED\n " : (),
  7627. $whitelisted ? "WHITELISTED\n " : ()),
  7628. $s);
  7629. }
  7630. my($subject_insert); # concatenation of triggered subject tag strings
  7631. if ($do_subj || $do_subj_u) {
  7632. if ($do_subj_u) {
  7633. $subject_insert = c('undecipherable_subject_tag');
  7634. do_log(3,"adding $subject_insert, $any_undecipherable, $hold");
  7635. }
  7636. if ($do_subj) {
  7637. $subject_insert .= $do_tag2 && $subject_tag2 ne '' ? $subject_tag2
  7638. : $subject_tag;
  7639. }
  7640. }
  7641. my($key) = join("\000", map {defined $_ ? $_ : ''} (
  7642. $do_tag_virus_checked, $do_tag_virus, $do_tag_banned, $do_tag_badh,
  7643. $do_tag, $do_tag2, $do_subj, $do_subj_u, $subject_insert,
  7644. $spam_level_bar, $full_spam_status) );
  7645. if ($first) {
  7646. ll(4) && do_log(4,sprintf(
  7647. "headers CLUSTERING: NEW CLUSTER <%s>: ".
  7648. "score=%s, tag=%s, tag2=%s, subj=%s, subj_u=%s, local=%s, bl=%s, s=%s",
  7649. $recip,
  7650. (!defined $spam_level ? 'x'
  7651. : !defined $boost ? $spam_level
  7652. : $boost >= 0 ? $spam_level.'+'.$boost : $spam_level.$boost),
  7653. $do_tag, $do_tag2, $do_subj, $do_subj_u, $is_local, $blacklisted,
  7654. $subject_insert));
  7655. $cluster_key = $key; $cluster_full_spam_status = $full_spam_status;
  7656. } elsif ($key eq $cluster_key) {
  7657. do_log(5,"headers CLUSTERING: <$recip> joining cluster");
  7658. } else {
  7659. do_log(5,"headers CLUSTERING: skipping <$recip> (tag=$do_tag, tag2=$do_tag2)");
  7660. next; # this recipient will be handled in some later pass
  7661. }
  7662. if ($first) { # insert headers required for the new cluster
  7663. if ($do_tag_virus_checked) {
  7664. $hdr_edits->append_header(c('X_HEADER_TAG'), c('X_HEADER_LINE'));
  7665. }
  7666. if ($do_tag_virus) {
  7667. $hdr_edits->append_header('X-Amavis-Alert',
  7668. "INFECTED, message contains virus:\n " . join(",\n ",@virusname), 1);
  7669. }
  7670. if ($do_tag_banned) {
  7671. my(@b); @b = @{$r->banned_parts} if defined $r->banned_parts;
  7672. my($b_chopped) = @b > 2; @b = (@b[0,1],'...') if $b_chopped;
  7673. my($msg) = "BANNED, message contains " . (@b==1 ? 'part' : 'parts') .
  7674. ":\n " . join(",\n ", @b) . ($b_chopped ? ", ..." : "");
  7675. $msg =~ s/[ \t]{6,}/ ... /g;
  7676. $hdr_edits->append_header('X-Amavis-Alert', $msg, 1);
  7677. }
  7678. if ($do_tag_badh) {
  7679. $hdr_edits->append_header('X-Amavis-Alert',
  7680. 'BAD HEADER '.$bad_headers[0], 1);
  7681. }
  7682. if ($do_tag) {
  7683. $hdr_edits->append_header('X-Spam-Status', $full_spam_status, 1);
  7684. $hdr_edits->append_header('X-Spam-Flag', 'YES');
  7685. $hdr_edits->append_header('X-Spam-Score',
  7686. !defined $spam_level ? '-' : 0+sprintf("%.3f",$spam_level+$boost) );
  7687. $hdr_edits->append_header('X-Spam-Level',
  7688. $spam_level_bar) if defined $spam_level_bar;
  7689. } elsif ($do_tag2) {
  7690. $hdr_edits->append_header('X-Spam-Status', $full_spam_status, 1);
  7691. $hdr_edits->append_header('X-Spam-Flag', 'YES');
  7692. $hdr_edits->append_header('X-Spam-Score',
  7693. !defined $spam_level ? '-' : 0+sprintf("%.3f",$spam_level+$boost) );
  7694. $hdr_edits->append_header('X-Spam-Level',
  7695. $spam_level_bar) if defined $spam_level_bar;
  7696. $hdr_edits->append_header('X-Spam-Report', $spam_report,1)
  7697. if $spam_report ne '' && c('sa_spam_report_header');
  7698. } elsif (!$bypassed) {
  7699. $hdr_edits->append_header('X-Spam-Status', $full_spam_status, 1);
  7700. $hdr_edits->append_header('X-Spam-Level',
  7701. $spam_level_bar) if defined $spam_level_bar;
  7702. }
  7703. if ($do_subj || $do_subj_u) {
  7704. my($entity) = $msginfo->mime_entity;
  7705. if (defined $entity && defined $entity->head->get('Subject',0)) {
  7706. $hdr_edits->edit_header('Subject',
  7707. sub { $_[1]=~/^([ \t]?)(.*)\z/s; ' '.$subject_insert.$2 });
  7708. } else { # no Subject header field present, insert one
  7709. $subject_insert =~ s/[ \t]+\z//; # trim
  7710. $hdr_edits->append_header('Subject', $subject_insert);
  7711. if (!defined $entity) {
  7712. do_log(-1,"WARN: no MIME entity!? Inserting 'Subject'");
  7713. } else {
  7714. do_log(0,"INFO: no existing header field 'Subject', inserting it");
  7715. }
  7716. }
  7717. }
  7718. }
  7719. push(@recip_cluster,$r); $first = 0;
  7720. my($delim) = c('recipient_delimiter');
  7721. if ($delim ne '' && $is_local) {
  7722. # append address extensions to mailbox names if desired
  7723. my($ext_map) = $do_tag_virus ? ca('addr_extension_virus_maps')
  7724. : $do_tag_banned ? ca('addr_extension_banned_maps')
  7725. : $do_tag2 ? ca('addr_extension_spam_maps')
  7726. : $do_tag_badh ? ca('addr_extension_bad_header_maps')
  7727. : undef;
  7728. my($ext) = !ref($ext_map) ? undef : lookup(0,$recip, @$ext_map);
  7729. if ($ext ne '') {
  7730. my($orig_extension); my($localpart,$domain) = split_address($recip);
  7731. ($localpart,$orig_extension) = split_localpart($localpart,$delim)
  7732. if c('replace_existing_extension'); # strip existing extension
  7733. my($new_addr) = $localpart.$delim.$ext.$domain;
  7734. ll(5) && do_log(5, (!defined($orig_extension) ? "appending addr ext"
  7735. : "replacing addr ext '$orig_extension' by")
  7736. . " '$ext', giving '$new_addr'");
  7737. $r->recip_addr_modified($new_addr);
  7738. }
  7739. }
  7740. }
  7741. my($done_all);
  7742. if (@recip_cluster == $per_recip_data_len) {
  7743. do_log(5,"headers CLUSTERING: " .
  7744. "done all $per_recip_data_len recips in one go");
  7745. $done_all = 1;
  7746. } else {
  7747. ll(4) && do_log(4,sprintf(
  7748. "headers CLUSTERING: got %d recips out of %d: %s",
  7749. scalar(@recip_cluster), $per_recip_data_len,
  7750. join(", ", map { "<" . $_->recip_addr . ">" } @recip_cluster) ));
  7751. }
  7752. my($s) = $cluster_full_spam_status; $s =~ s/\n[ \t]/ /g;
  7753. ll(2) && do_log(2,sprintf("SPAM-TAG, %s -> %s, %s",
  7754. qquote_rfc2821_local($msginfo->sender),
  7755. join(',', qquote_rfc2821_local(
  7756. map { $_->recip_addr } @recip_cluster)), $s));
  7757. ($hdr_edits, \@recip_cluster, $done_all);
  7758. }
  7759. sub do_quarantine($$$$$;$) {
  7760. my($conn,$msginfo,$hdr_edits,$recips_ref,$quarantine_method,$snmp_id) = @_;
  7761. if ($quarantine_method eq '') { do_log(5, "quarantine disabled") }
  7762. else {
  7763. my($sender) = $msginfo->sender;
  7764. my($quar_msg) = Amavis::In::Message->new;
  7765. $quar_msg->rx_time($msginfo->rx_time); # copy the reception time
  7766. $quar_msg->body_type($msginfo->body_type); # use the same BODY= type
  7767. $quar_msg->mail_id($msginfo->mail_id); # use the same the mail_id
  7768. $quar_msg->body_digest($msginfo->body_digest); # copy original digest
  7769. $quar_msg->delivery_method($quarantine_method);
  7770. if ($quarantine_method =~ /^(bsmtp|sql):/i) {
  7771. $quar_msg->sender($sender); # original sender & recipients
  7772. $quar_msg->recips($msginfo->recips);
  7773. } else {
  7774. my($mftq) = c('mailfrom_to_quarantine');
  7775. $quar_msg->sender(defined $mftq ? $mftq : $sender);
  7776. $quar_msg->recips($recips_ref); # e.g. per-recip quarantine
  7777. }
  7778. $hdr_edits = Amavis::Out::EditHeader->new if !defined($hdr_edits);
  7779. $hdr_edits->prepend_header('X-Quarantine-Id', '<'.$msginfo->mail_id.'>');
  7780. if ($quarantine_method =~ /^bsmtp:/i) { # X-Envelope-* would be redundant
  7781. } else {
  7782. # NOTE: RFC2821 mentions possible headers X-SMTP-MAIL and X-SMTP-RCPT
  7783. # Exim uses: Envelope-To, Sendmail uses X-Envelope-To;
  7784. # No need with bsmtp or sql, which carry addresses in the envelope
  7785. $hdr_edits->prepend_header('X-Envelope-To',
  7786. join(",\n ", qquote_rfc2821_local(@{$msginfo->recips})), 1);
  7787. $hdr_edits->prepend_header('X-Envelope-From',
  7788. qquote_rfc2821_local($sender));
  7789. }
  7790. do_log(5, "DO_QUARANTINE, sender: " . $quar_msg->sender);
  7791. $quar_msg->auth_submitter(quote_rfc2821_local($quar_msg->sender));
  7792. $quar_msg->auth_user(c('amavis_auth_user'));
  7793. $quar_msg->auth_pass(c('amavis_auth_pass'));
  7794. $quar_msg->header_edits($hdr_edits);
  7795. $quar_msg->mail_text($msginfo->mail_text); # use the same mail contents
  7796. snmp_count('QuarMsgs');
  7797. mail_dispatch($conn, $quar_msg, 1, 0);
  7798. my($n_smtp_resp, $n_exit_code, $n_dsn_needed) =
  7799. one_response_for_all($quar_msg, 0, am_id()); # check status
  7800. if ($n_smtp_resp =~ /^2/ && !$n_dsn_needed) { # ok
  7801. snmp_count($snmp_id eq '' ? 'QuarOther' : $snmp_id);
  7802. } elsif ($n_smtp_resp =~ /^4/) {
  7803. snmp_count('QuarAttemptTempFails');
  7804. die "temporarily unable to quarantine: $n_smtp_resp";
  7805. } else { # abort if quarantining not successful
  7806. snmp_count('QuarAttemptFails');
  7807. die "Can not quarantine: $n_smtp_resp";
  7808. }
  7809. my($quar_type);
  7810. my(@qa); my(%seen); # collect unique quarantine mailboxes or addresses
  7811. my($existing_qa) = $msginfo->quarantined_to;
  7812. if (ref $existing_qa) { @qa = @$existing_qa; $seen{$_}++ for (@qa) }
  7813. for my $r (@{$quar_msg->per_recip_data}) {
  7814. my($mbxname) = $r->recip_mbxname;
  7815. if ($mbxname ne '' && !$seen{$mbxname}++) {
  7816. push(@qa,$mbxname);
  7817. $quar_type = /^bsmtp:/ ? 'B' : /^smtp:/ ? 'M' : /^sql:/ ? 'Q' :
  7818. /^local:/ ? ($mbxname=~/\@/ ? 'M' : $mbxname=~/\.gz\z/ ? 'Z' : 'F')
  7819. : '?' for (lc($quarantine_method));
  7820. }
  7821. }
  7822. $msginfo->quar_type($quar_type);
  7823. $msginfo->quarantined_to(\@qa); # remember where it was quarantined to
  7824. do_log(5, "DO_QUARANTINE done");
  7825. }
  7826. }
  7827. # if virus/banned/bad-header found - quarantine it and send notifications
  7828. sub do_virus($$$) {
  7829. my($conn, $msginfo, $virus_dejavu) = @_;
  7830. my($q_method, $quarantine_to_maps_ref, $admin_maps_ref) =
  7831. @virusname ?
  7832. (c('virus_quarantine_method'),
  7833. ca('virus_quarantine_to_maps'),
  7834. ca('virus_admin_maps') )
  7835. : $banned_filename_any ?
  7836. (c('banned_files_quarantine_method'),
  7837. ca('banned_quarantine_to_maps'),
  7838. ca('banned_admin_maps') )
  7839. : @bad_headers ?
  7840. (c('bad_header_quarantine_method'),
  7841. ca('bad_header_quarantine_to_maps'),
  7842. ca('bad_header_admin_maps') )
  7843. : (undef, undef, undef, undef);
  7844. do_log(5, "do_virus: looking for per-recipient quarantine and admins");
  7845. my($newvirus_admin_maps_ref) =
  7846. @virusname && !$virus_dejavu ? ca('newvirus_admin_maps') : undef;
  7847. my(@q_addr,@a_addr); # get per-recipient quarantine address(es) and admins
  7848. for my $r (@{$msginfo->per_recip_data}) {
  7849. my($rec) = $r->recip_addr;
  7850. my($q); # quarantine (pseudo) address associated with the recipient
  7851. my($a); # administrator's e-mail address
  7852. ($q) = lookup(0,$rec,@$quarantine_to_maps_ref) if $quarantine_to_maps_ref;
  7853. $q = $rec if $q ne '' && $q_method =~ /^bsmtp:/i; # orig.recip when BSMTP
  7854. ($a) = lookup(0,$rec,@$admin_maps_ref) if $admin_maps_ref;
  7855. push(@q_addr, $q) if defined $q && $q ne '' && !grep {$_ eq $q} @q_addr;
  7856. push(@a_addr, $a) if defined $a && $a ne '' && !grep {$_ eq $a} @a_addr;
  7857. if ($newvirus_admin_maps_ref) {
  7858. ($a) = lookup(0,$rec,@$newvirus_admin_maps_ref);
  7859. push(@a_addr, $a) if defined $a && $a ne '' && !grep {$_ eq $a} @a_addr;
  7860. }
  7861. }
  7862. if (@q_addr) { # do the quarantining
  7863. # prepare header edits for the quarantined message
  7864. my($hdr_edits) = Amavis::Out::EditHeader->new;
  7865. if (@virusname) {
  7866. $hdr_edits->append_header('X-Amavis-Alert',
  7867. "INFECTED, message contains virus:\n " . join(",\n ", @virusname), 1);
  7868. }
  7869. for my $r (@{$msginfo->per_recip_data}) {
  7870. my(@b); @b = @{$r->banned_parts} if defined $r->banned_parts;
  7871. if (@b) {
  7872. my($b_chopped) = @b > 3; @b = @b[0..2] if $b_chopped;
  7873. my($msg) = "BANNED, message contains " . (@b==1 ? 'part' : 'parts') .
  7874. ":\n " . join(",\n ", @b) . ($b_chopped ? ", ..." : "");
  7875. $msg =~ s/[ \t]{6,}/ ... /g;
  7876. $hdr_edits->append_header('X-Amavis-Alert', $msg, 1);
  7877. last; # ***fudge: only the first recipient's banned hit will be shown
  7878. }
  7879. }
  7880. if (@bad_headers) {
  7881. $hdr_edits->append_header('X-Amavis-Alert',
  7882. 'BAD HEADER '.$bad_headers[0], 1);
  7883. }
  7884. do_quarantine($conn,$msginfo,$hdr_edits,\@q_addr,$q_method,
  7885. @virusname ? 'QuarVirusMsgs' :
  7886. $banned_filename_any ? 'QuarBannedMsgs' :
  7887. @bad_headers ? 'QuarBadHMsgs' : 'QuarOther');
  7888. }
  7889. my($hdr_edits) = Amavis::Out::EditHeader->new;
  7890. if (!@a_addr) {
  7891. do_log(4, "Skip admin notification, no administrators");
  7892. } else { # notify per-recipient virus administrators
  7893. ll(5) && do_log(5, sprintf("DO_VIRUS - NOTIFICATIONS to %s; sender: %s",
  7894. join(",",qquote_rfc2821_local(@a_addr)), $msginfo->sender));
  7895. my($notification) = Amavis::In::Message->new;
  7896. $notification->rx_time($msginfo->rx_time); # copy the reception time
  7897. $notification->delivery_method(c('notify_method'));
  7898. $notification->sender(c('mailfrom_notify_admin'));
  7899. $notification->auth_submitter(
  7900. quote_rfc2821_local(c('mailfrom_notify_admin')));
  7901. $notification->auth_user(c('amavis_auth_user'));
  7902. $notification->auth_pass(c('amavis_auth_pass'));
  7903. $notification->recips([@a_addr]);
  7904. my(%mybuiltins) = %builtins; # make a local copy
  7905. $mybuiltins{'T'} = \@a_addr; # used in 'To:'
  7906. $mybuiltins{'f'} = c('hdrfrom_notify_admin'); # From:
  7907. $notification->mail_text(
  7908. string_to_mime_entity(expand(cr('notify_virus_admin_templ'),
  7909. \%mybuiltins)));
  7910. # $notification->body_type('7BIT');
  7911. $notification->header_edits($hdr_edits);
  7912. mail_dispatch($conn, $notification, 1, 0);
  7913. my($n_smtp_resp, $n_exit_code, $n_dsn_needed) =
  7914. one_response_for_all($notification, 0, am_id()); # check status
  7915. if ($n_smtp_resp =~ /^2/ && !$n_dsn_needed) { # ok
  7916. } elsif ($n_smtp_resp =~ /^4/) {
  7917. die "temporarily unable to notify virus admin: $n_smtp_resp";
  7918. } else {
  7919. do_log(-1, "FAILED to notify virus admin: $n_smtp_resp");
  7920. }
  7921. # $notification->purge;
  7922. }
  7923. for my $r (@{$msginfo->per_recip_data}) {
  7924. my($wr) = 0; my($rec) = $r->recip_addr;
  7925. if (!c('warn_offsite') && !lookup(0,$rec,@{ca('local_domains_maps')})) {
  7926. # not notifying foreign recipients
  7927. # } elsif (! defined($msginfo->sender_contact) ) { # (not general enough)
  7928. # do_log(5,"do_virus: skip recip notifications for unknown sender");
  7929. } elsif ($r->infected) {
  7930. $wr = lookup(0,$rec,@{ca('warnvirusrecip_maps')});
  7931. } elsif (defined($r->banned_parts) && @{$r->banned_parts}) {
  7932. $wr = lookup(0,$rec,@{ca('warnbannedrecip_maps')});
  7933. } elsif (@bad_headers &&
  7934. !lookup(0,$rec,@{ca('bypass_header_checks_maps')})) {
  7935. $wr = lookup(0,$rec,@{ca('warnbadhrecip_maps')});
  7936. }
  7937. if ($wr) { # warn recipient
  7938. my($notification) = Amavis::In::Message->new;
  7939. $notification->rx_time($msginfo->rx_time); # copy the reception time
  7940. $notification->delivery_method(c('notify_method'));
  7941. $notification->sender(c('mailfrom_notify_recip'));
  7942. $notification->auth_submitter(
  7943. quote_rfc2821_local(c('mailfrom_notify_recip')));
  7944. $notification->auth_user(c('amavis_auth_user'));
  7945. $notification->auth_pass(c('amavis_auth_pass'));
  7946. $notification->recips([$rec]);
  7947. my(@b); @b = @{$r->banned_parts} if defined $r->banned_parts;
  7948. my($b_chopped) = @b > 2; @b = (@b[0,1],'...') if $b_chopped;
  7949. s/[ \t]{6,}/ ... /g for @b;
  7950. my(%mybuiltins) = %builtins; # make a local copy
  7951. $mybuiltins{'F'} = \@b; # list of banned file names
  7952. $mybuiltins{'f'} = c('hdrfrom_notify_recip'); # 'From:'
  7953. $mybuiltins{'T'} = quote_rfc2821_local($rec); # 'To:'
  7954. my $foo = expand(cr('notify_virus_recips_templ'), \%mybuiltins);
  7955. my $bar = cr('notify_virus_recips_templ');
  7956. warn "++++++++++ . ". $$foo . "+++++++++";
  7957. warn "-----------" . $$bar . "------";
  7958. $notification->mail_text(
  7959. string_to_mime_entity(expand(cr('notify_virus_recips_templ'),
  7960. \%mybuiltins)) );
  7961. # $notification->body_type('7BIT');
  7962. $notification->header_edits($hdr_edits);
  7963. mail_dispatch($conn, $notification, 1, 0);
  7964. my($n_smtp_resp, $n_exit_code, $n_dsn_needed) =
  7965. one_response_for_all($notification, 0, am_id()); # check status
  7966. if ($n_smtp_resp =~ /^2/ && !$n_dsn_needed) { # ok
  7967. } elsif ($n_smtp_resp =~ /^4/) {
  7968. die "temporarily unable to notify recipient rec: $n_smtp_resp";
  7969. } else {
  7970. do_log(-1, "FAILED to notify recipient $rec: $n_smtp_resp");
  7971. }
  7972. # $notification->purge;
  7973. }
  7974. }
  7975. do_log(5, "DO_VIRUS - DONE");
  7976. }
  7977. #
  7978. # if spam found - quarantine it and log report
  7979. sub do_spam($$$$$$) {
  7980. my($conn, $msginfo,
  7981. $spam_level, $spam_status, $spam_report, $autolearn_status) = @_;
  7982. my($q_method) = c('spam_quarantine_method');
  7983. # use the smallest value as the level reported in quarantined headers!
  7984. my($tag_level) =
  7985. min(map { scalar(lookup(0,$_,@{ca('spam_tag_level_maps')})) } @{$msginfo->recips});
  7986. my($tag2_level) =
  7987. min(map { scalar(lookup(0,$_,@{ca('spam_tag2_level_maps')})) } @{$msginfo->recips});
  7988. my($kill_level) =
  7989. min(map { scalar(lookup(0,$_,@{ca('spam_kill_level_maps')})) } @{$msginfo->recips});
  7990. my($blacklisted) =
  7991. scalar(grep { $_->recip_blacklisted_sender } @{$msginfo->per_recip_data});
  7992. my($whitelisted) =
  7993. scalar(grep { $_->recip_whitelisted_sender } @{$msginfo->per_recip_data});
  7994. my($s) = $spam_status; $s =~ s/,/,\n /g; # allow header field wrapping
  7995. my(@boost) = map { $_->recip_score_boost } @{$msginfo->per_recip_data};
  7996. my($full_spam_status) = sprintf(
  7997. "%s,\n score=%s\n tag=%s\n tag2=%s\n kill=%s\n %s%s",
  7998. (defined $spam_level && defined $tag2_level && $spam_level>=$tag2_level ?
  7999. 'Yes' : 'No'),
  8000. (map { !defined $_ ? 'x' : 0+sprintf("%.3f",$_) }
  8001. ($spam_level+max(@boost), $tag_level, $tag2_level, $kill_level)),
  8002. join('', $blacklisted ? "BLACKLISTED\n " : (),
  8003. $whitelisted ? "WHITELISTED\n " : ()),
  8004. $s);
  8005. do_log(5, "do_spam: looking for a quarantine address");
  8006. my(@q_addr,@a_addr); # quarantine address(es) and administrators
  8007. my($sqbsm) = ca('spam_quarantine_bysender_to_maps');
  8008. if (@$sqbsm) { # by-sender quarantine
  8009. my($q); $q = lookup(0,$msginfo->sender, @$sqbsm);
  8010. push(@q_addr, $q) if defined $q && $q ne '' && !grep {$_ eq $q} @q_addr;
  8011. }
  8012. # get per-recipient quarantine address(es) and admins
  8013. for my $r (@{$msginfo->per_recip_data}) {
  8014. my($rec) = $r->recip_addr;
  8015. my($q); # quarantine (pseudo) address associated with the recipient
  8016. ($q) = lookup(0,$rec, @{ca('spam_quarantine_to_maps')});
  8017. if ($q ne '' && defined $spam_level) {
  8018. my($cutoff) = lookup(0,$rec,@{ca('spam_quarantine_cutoff_level_maps')});
  8019. if (!defined $cutoff || $cutoff eq '') {}
  8020. elsif ($spam_level + $r->recip_score_boost >= $cutoff) {
  8021. do_log(2, "do_spam: spam level exceeds quarantine cutoff level $cutoff");
  8022. $q = ''; # disable quarantine on behalf of this recipient
  8023. }
  8024. }
  8025. $q = $rec if $q ne '' && $q_method =~ /^bsmtp:/i; # orig.recip when BSMTP
  8026. my($a) = lookup(0,$rec, @{ca('spam_admin_maps')});
  8027. push(@q_addr, $q) if defined $q && $q ne '' && !grep {$_ eq $q} @q_addr;
  8028. push(@a_addr, $a) if defined $a && $a ne '' && !grep {$_ eq $a} @a_addr;
  8029. }
  8030. if (@q_addr) { # do the quarantining
  8031. # prepare header edits for the quarantined message
  8032. my($hdr_edits) = Amavis::Out::EditHeader->new;
  8033. $hdr_edits->append_header('X-Spam-Status', $full_spam_status, 1);
  8034. $hdr_edits->append_header('X-Spam-Score',
  8035. !defined $spam_level ? '-' : 0+sprintf("%.3f",$spam_level+max(@boost)) );
  8036. my($slc) = c('sa_spam_level_char');
  8037. $hdr_edits->append_header('X-Spam-Level',
  8038. $slc x min(0+$spam_level,64)) if $slc ne '';
  8039. $hdr_edits->append_header('X-Spam-Flag', !$whitelisted &&
  8040. ($blacklisted || (defined $spam_level && defined $tag2_level &&
  8041. $spam_level >= $tag2_level)) ? 'YES' : 'NO');
  8042. $hdr_edits->append_header('X-Spam-Report', $spam_report,1)
  8043. if c('sa_spam_report_header') && $spam_report ne '';
  8044. do_quarantine($conn,$msginfo,$hdr_edits,\@q_addr,$q_method,'QuarSpamMsgs');
  8045. }
  8046. $s = $full_spam_status; $s =~ s/\n[ \t]/ /g;
  8047. ll(2) && do_log(2,sprintf("SPAM, %s -> %s, %s%s%s",
  8048. qquote_rfc2821_local($msginfo->sender_source),
  8049. join(',', qquote_rfc2821_local(@{$msginfo->recips})), $s,
  8050. $autolearn_status eq '' ? '' : ", autolearn=$autolearn_status",
  8051. !@q_addr ? '' : sprintf(", quarantine %s (%s)",
  8052. $msginfo->mail_id, join(',',@q_addr)) ));
  8053. if (!@a_addr) {
  8054. do_log(4, "Skip spam admin notification, no administrators");
  8055. } else { # notify per-recipient spam administrators
  8056. ll(5) && do_log(5, sprintf("DO_SPAM - NOTIFICATIONS to %s; sender: %s",
  8057. join(",",qquote_rfc2821_local(@a_addr)), $msginfo->sender));
  8058. my($notification) = Amavis::In::Message->new;
  8059. $notification->rx_time($msginfo->rx_time); # copy the reception time
  8060. $notification->delivery_method(c('notify_method'));
  8061. $notification->sender(c('mailfrom_notify_spamadmin'));
  8062. $notification->auth_submitter(
  8063. quote_rfc2821_local(c('mailfrom_notify_spamadmin')));
  8064. $notification->auth_user(c('amavis_auth_user'));
  8065. $notification->auth_pass(c('amavis_auth_pass'));
  8066. $notification->recips([@a_addr]);
  8067. my(%mybuiltins) = %builtins; # make a local copy
  8068. $mybuiltins{'T'} = \@a_addr; # used in 'To:'
  8069. $mybuiltins{'f'} = c('hdrfrom_notify_spamadmin');
  8070. $notification->mail_text(
  8071. string_to_mime_entity(expand(cr('notify_spam_admin_templ'),
  8072. \%mybuiltins)));
  8073. # $notification->body_type('7BIT');
  8074. my($hdr_edits) = Amavis::Out::EditHeader->new;
  8075. $notification->header_edits($hdr_edits);
  8076. mail_dispatch($conn, $notification, 1, 0);
  8077. my($n_smtp_resp, $n_exit_code, $n_dsn_needed) =
  8078. one_response_for_all($notification, 0, am_id()); # check status
  8079. if ($n_smtp_resp =~ /^2/ && !$n_dsn_needed) { # ok
  8080. } elsif ($n_smtp_resp =~ /^4/) {
  8081. die "temporarily unable to notify spam admin: $n_smtp_resp";
  8082. } else {
  8083. do_log(-1, "FAILED to notify spam admin: $n_smtp_resp");
  8084. }
  8085. # $notification->purge;
  8086. }
  8087. do_log(5, "DO_SPAM DONE");
  8088. }
  8089. # Calculate message digest;
  8090. # While at it, also get message size, check for 8-bit data, and store original
  8091. # header, since we need it for the %H macro, and MIME::Tools may modify it.
  8092. #
  8093. sub get_body_digest($$) {
  8094. my($fh, $msginfo) = @_;
  8095. $fh->seek(0,0) or die "Can't rewind mail file: $!";
  8096. # choose message digest method:
  8097. my($hctx) = Digest::MD5->new; # 128 bits (32 hex digits)
  8098. my($bctx) = Digest::MD5->new; # 128 bits (32 hex digits)
  8099. # my($bctx) = Digest::SHA1->new; # 160 bits (40 hex digits), slightly slower
  8100. my($h_8bit,$b_8bit) = (0,0);
  8101. my(@orig_header); my($header_size)=0; my($body_size)=0; my($ln);
  8102. for (undef $!; defined($ln=<$fh>); undef $!) { # skip mail header
  8103. last if $ln eq $eol;
  8104. $header_size += length($ln);
  8105. $ln=~/^[\000-\177]*\z/ or $h_8bit=1;
  8106. $hctx->add($ln); push(@orig_header,$ln); # with trailing EOL
  8107. }
  8108. defined $ln || $!==0 or die "Error reading mail header: $!";
  8109. add_entropy($hctx->digest); # faster than traversing @orig_header again
  8110. my($len);
  8111. while (($len = read($fh,$_,16384)) > 0) {
  8112. $bctx->add($_); $body_size += $len;
  8113. /^[\000-\177]*\z/ or $b_8bit=1; # much faster than !/[^\000-\177]/
  8114. }
  8115. defined $len or die "Error reading mail body: $!";
  8116. my($signature) = $bctx->hexdigest;
  8117. # my($signature) = $bctx->b64digest;
  8118. add_entropy($signature);
  8119. $signature = untaint($signature) # checked (either 32 or 40 char)
  8120. if $signature =~ /^ [0-9a-fA-F]{32} (?: [0-9a-fA-F]{8} )? \z/x;
  8121. # store information obtained
  8122. $msginfo->orig_header(\@orig_header);
  8123. $msginfo->orig_header_size($header_size);
  8124. $msginfo->orig_body_size($body_size);
  8125. $msginfo->body_digest($signature);
  8126. # check for 8-bit characters and adjust body type if necessary (rfc1652)
  8127. my($bt_orig) = $msginfo->body_type;
  8128. my($bt_true) = $h_8bit || $b_8bit ? '8BITMIME' : '7BIT';
  8129. if (!defined($bt_orig) || $bt_orig eq '') {
  8130. do_log(4,"setting body type: $bt_true ($h_8bit,$b_8bit)");
  8131. $msginfo->body_type($bt_true);
  8132. } elsif ($bt_true eq '8BITMIME' && uc($bt_orig) ne '8BITMIME') {
  8133. do_log(4,"changing body type: $bt_orig => $bt_true ($h_8bit,$b_8bit)");
  8134. $msginfo->body_type($bt_true);
  8135. }
  8136. do_log(3, "body hash: $signature");
  8137. section_time('body_digest');
  8138. $signature;
  8139. }
  8140. sub find_program_path($$$) {
  8141. my($fv_list, $path_list_ref, $may_log) = @_;
  8142. $fv_list = [$fv_list] if !ref $fv_list;
  8143. my($found);
  8144. for my $fv (@$fv_list) {
  8145. my(@fv_cmd) = split(' ',$fv);
  8146. if (!@fv_cmd) { # empty, not available
  8147. } elsif ($fv_cmd[0] =~ /^\//) { # absolute path
  8148. my($errn) = stat($fv_cmd[0]) ? 0 : 0+$!;
  8149. if ($errn == ENOENT) { }
  8150. elsif ($errn) {
  8151. do_log(-1, "find_program_path: " . "$fv_cmd[0] inaccessible: $!")
  8152. if $may_log;
  8153. } elsif (-x _ && !-d _) { $found = join(' ', @fv_cmd) }
  8154. } elsif ($fv_cmd[0] =~ /\//) { # relative path
  8155. die "find_program_path: relative paths not implemented: @fv_cmd\n";
  8156. } else { # walk through the specified PATH
  8157. for my $p (@$path_list_ref) {
  8158. my($errn) = stat("$p/$fv_cmd[0]") ? 0 : 0+$!;
  8159. if ($errn == ENOENT) { }
  8160. elsif ($errn) {
  8161. do_log(-1, "find_program_path: " . "$p/$fv_cmd[0] inaccessible: $!")
  8162. if $may_log;
  8163. } elsif (-x _ && !-d _) {
  8164. $found = $p . '/' . join(' ', @fv_cmd);
  8165. last;
  8166. }
  8167. }
  8168. }
  8169. last if defined $found;
  8170. }
  8171. $found;
  8172. }
  8173. sub find_external_programs($) {
  8174. my($path_list_ref) = @_;
  8175. for my $f (qw($file $dspam)) {
  8176. my($g) = $f; $g =~ s/\$/Amavis::Conf::/; my($fv_list) = eval('$' . $g);
  8177. my($found) = find_program_path($fv_list, $path_list_ref, 1);
  8178. { no strict 'refs'; $$g = $found } # NOTE: a symbolic reference
  8179. if (!defined $found) { do_log(-1,sprintf("No %-19s not using it", "$f,")) }
  8180. else {
  8181. do_log(0,sprintf("Found %-16s at %s%s", $f,
  8182. $daemon_chroot_dir ne '' ? "(chroot: $daemon_chroot_dir/) " : '',
  8183. $found));
  8184. }
  8185. }
  8186. # map program name path hints to full paths for decoders
  8187. my(%any_st);
  8188. for my $f (@{ca('decoders')}) {
  8189. next if !defined $f || !ref $f; # empty, skip
  8190. my($short_type) = $f->[0]; my(@tried,@found); my($any) = 0;
  8191. for my $d (@$f[2..$#$f]) { # all but the first two elements are programs
  8192. # allow one level of indirection
  8193. my($dd) = (ref $d eq 'SCALAR' || ref $d eq 'REF') ? $$d : $d;
  8194. my($found) = find_program_path($dd, $path_list_ref, 1);
  8195. if (defined $found) { $any++; $dd = $found; $d = $dd; push(@found,$dd) }
  8196. else { push(@tried, !ref($dd) ? $dd : join(", ",@$dd)) if $dd ne '' }
  8197. }
  8198. my($is_a_backup) = $any_st{$short_type};
  8199. my($ll,$tier) = !$is_a_backup ? (0,'') : (2,' (backup, not used)');
  8200. if (@$f <= 2) { # no external programs specified
  8201. do_log($ll, sprintf("Internal decoder for .%-4s%s", $short_type,$tier));
  8202. $f = undef if $is_a_backup; # discard a backup entry
  8203. } elsif (!$any) { # external programs specified but none found
  8204. do_log($ll, sprintf("No decoder for .%-4s%s", $short_type,
  8205. !@tried ? '' : ' tried: '.join("; ",@tried))) if !$is_a_backup;
  8206. $f = undef; # release its storage
  8207. } else {
  8208. do_log($ll, sprintf("Found decoder for .%-4s at %s%s%s", $short_type,
  8209. $daemon_chroot_dir ne '' ? "(chroot: $daemon_chroot_dir/) " : '',
  8210. join("; ",@found), $tier));
  8211. $f = undef if $is_a_backup; # discard a backup entry
  8212. }
  8213. $any_st{$short_type}++ if defined $f;
  8214. }
  8215. # map program name hints to full paths - av scanners
  8216. my($tier) = 'primary'; # primary, secondary, ... av scanners
  8217. for my $f (@{ca('av_scanners')}, "\000", @{ca('av_scanners_backup')}) {
  8218. if ($f eq "\000") { # next tier
  8219. $tier = 'secondary';
  8220. } elsif (!defined $f || !ref $f) { # empty, skip
  8221. } elsif (ref($f->[1]) eq 'CODE') {
  8222. do_log(0, "Using internal av scanner code for ($tier) " . $f->[0]);
  8223. } else {
  8224. my($found) = $f->[1] = find_program_path($f->[1], $path_list_ref, 1);
  8225. if (!defined $found) {
  8226. do_log(3, "No $tier av scanner: " . $f->[0]);
  8227. $f = undef; # release its storage
  8228. } else {
  8229. do_log(0, sprintf("Found $tier av scanner %-11s at %s%s", $f->[0],
  8230. $daemon_chroot_dir ne '' ? "(chroot: $daemon_chroot_dir/) " : '',
  8231. $found));
  8232. }
  8233. }
  8234. }
  8235. }
  8236. # Fetch remaining modules, all must be loaded before chroot and fork occurs
  8237. sub fetch_modules_extra() {
  8238. my(@modules);
  8239. if ($extra_code_sql_base) {
  8240. push(@modules, 'DBI');
  8241. for (@lookup_sql_dsn, @storage_sql_dsn) {
  8242. my(@dsn) = split(/:/,$_->[0],-1);
  8243. push(@modules, 'DBD::'.$dsn[1]) if uc($dsn[0]) eq 'DBI';
  8244. }
  8245. }
  8246. push(@modules, qw(Net::LDAP Net::LDAP::Util Net::LDAP::Search))
  8247. if $extra_code_ldap;
  8248. if (c('bypass_decode_parts') &&
  8249. !grep {exists $policy_bank{$_}{'bypass_decode_parts'} &&
  8250. !$policy_bank{$_}{'bypass_decode_parts'} } keys %policy_bank) {
  8251. } else {
  8252. push(@modules, qw(Convert::TNEF Convert::UUlib Archive::Zip Archive::Tar));
  8253. }
  8254. push(@modules, 'Mail::SpamAssassin') if $extra_code_antispam;
  8255. push(@modules, 'Authen::SASL') if c('auth_required_out');
  8256. Amavis::Boot::fetch_modules('REQUIRED ADDITIONAL MODULES', 1, @modules);
  8257. my($sa_version);
  8258. $sa_version = Mail::SpamAssassin::Version() if $extra_code_antispam;
  8259. @modules = (); # now start collecting optional modules
  8260. if ($unicode_aware) {
  8261. push(@modules, qw(
  8262. bytes bytes_heavy.pl utf8 utf8_heavy.pl
  8263. Encode Encode::Byte Encode::MIME::Header Encode::Unicode::UTF7
  8264. Encode::CN Encode::TW Encode::KR Encode::JP
  8265. unicore::Canonical.pl unicore::Exact.pl unicore::PVA.pl
  8266. unicore::To::Fold.pl unicore::To::Title.pl
  8267. unicore::To::Lower.pl unicore::To::Upper.pl
  8268. ));
  8269. }
  8270. if ($extra_code_antispam) {
  8271. push(@modules, qw(
  8272. Mail::SpamAssassin::Locker::Flock
  8273. Mail::SpamAssassin::Locker::UnixNFSSafe
  8274. Mail::SpamAssassin::DBBasedAddrList
  8275. Mail::SpamAssassin::SQLBasedAddrList
  8276. Mail::SpamAssassin::PersistentAddrList
  8277. Mail::SpamAssassin::PerMsgLearner
  8278. Mail::SpamAssassin::AutoWhitelist
  8279. Mail::SpamAssassin::BayesStore::DBM
  8280. Mail::SpamAssassin::BayesStore::SQL
  8281. Mail::SpamAssassin::Plugin::Hashcash
  8282. Mail::SpamAssassin::Plugin::RelayCountry
  8283. Mail::SpamAssassin::Plugin::SPF
  8284. Mail::SpamAssassin::Plugin::URIDNSBL
  8285. DBD::mysql Sys::Hostname::Long
  8286. Mail::SPF::Query Razor2::Client::Agent Net::CIDR::Lite
  8287. Net::DNS::RR::SOA Net::DNS::RR::NS Net::DNS::RR::MX
  8288. Net::DNS::RR::A Net::DNS::RR::AAAA Net::DNS::RR::PTR
  8289. Net::DNS::RR::CNAME Net::DNS::RR::TXT Net::Ping
  8290. ));
  8291. # ??? ArchiveIterator Reporter Data::Dumper Getopt::Long Sys::Syslog lib
  8292. # Mail::SpamAssassin::BayesStore::SDBM
  8293. }
  8294. if ($extra_code_antispam && defined $sa_version) {
  8295. # *** note that $sa_version could be 3.0.1, which is not really numeric!
  8296. if ($sa_version=~/^(\d+(?:\.\d+)?)/ && $1 < 3) { push(@modules, qw(
  8297. Mail::SpamAssassin::UnixLocker Mail::SpamAssassin::BayesStoreDBM
  8298. Mail::SpamAssassin::SpamCopURI
  8299. URI URI::Escape URI::Heuristic URI::QueryParam URI::Split URI::URL
  8300. URI::WithBase URI::_foreign URI::_generic URI::_ldap URI::_login
  8301. URI::_query URI::_segment URI::_server URI::_userpass URI::data URI::ftp
  8302. URI::gopher URI::http URI::https URI::ldap URI::ldapi URI::ldaps
  8303. URI::mailto URI::mms URI::news URI::nntp URI::pop URI::rlogin URI::rsync
  8304. URI::rtsp URI::rtspu URI::sip URI::sips URI::snews URI::ssh URI::telnet
  8305. URI::tn3270 URI::urn URI::urn::isbn URI::urn::oid
  8306. URI::file URI::file::Base URI::file::Unix URI::file::Win32
  8307. ));
  8308. } elsif ($sa_version=~/^(\d+(?:\.\d+)?)/ && $1 >= 3.1) { push(@modules, qw(
  8309. Mail::SpamAssassin::BayesStore::MySQL
  8310. Mail::SpamAssassin::Plugin::AutoLearnThreshold
  8311. Mail::SpamAssassin::Plugin::ReplaceTags
  8312. Mail::SpamAssassin::Plugin::MIMEHeader
  8313. Mail::SpamAssassin::Plugin::AWL Mail::SpamAssassin::Plugin::DCC
  8314. Mail::SpamAssassin::Plugin::Pyzor Mail::SpamAssassin::Plugin::Razor2
  8315. Mail::SpamAssassin::Plugin::SpamCop
  8316. Mail::SpamAssassin::Plugin::WhiteListSubject
  8317. Mail::SpamAssassin::Plugin::DomainKeys
  8318. Mail::DomainKeys::Header Mail::DomainKeys::Message
  8319. Mail::DomainKeys::Policy Mail::DomainKeys::Signature
  8320. Mail::DomainKeys::Key Mail::DomainKeys::Key::Public
  8321. Crypt::OpenSSL::RSA
  8322. auto::Crypt::OpenSSL::RSA::_new auto::Crypt::OpenSSL::RSA::DESTROY
  8323. auto::Crypt::OpenSSL::RSA::load_public_key
  8324. auto::Crypt::OpenSSL::RSA::new_public_key
  8325. IP::Country::Fast
  8326. ));
  8327. # BayesStore::PgSQL BayesStore::SDBM
  8328. # Plugin::AntiVirus Plugin::DomainKeys Plugin::NetCache Plugin::TextCat
  8329. }
  8330. }
  8331. my($missing);
  8332. $missing = Amavis::Boot::fetch_modules('PRE-COMPILE OPTIONAL MODULES', 0,
  8333. @modules) if @modules;
  8334. do_log(2, 'INFO: no optional modules: '.join(' ',@$missing))
  8335. if ref $missing && @$missing;
  8336. # load optional modules SAVI and Mail::ClamAV if available and requested
  8337. if ($extra_code_antivirus) {
  8338. my($clamav_module_ok);
  8339. for my $entry (@{ca('av_scanners')}, @{ca('av_scanners_backup')}) {
  8340. if (ref($entry) ne 'ARRAY') { # none
  8341. } elsif ($entry->[1] eq \&ask_sophos_savi ||
  8342. $entry->[1] eq \&sophos_savi ||
  8343. $entry->[0] eq 'Sophos SAVI') {
  8344. if (defined(eval { require SAVI }) && SAVI->VERSION(0.30) &&
  8345. Amavis::AV::sophos_savi_init(@$entry)) {} # ok, loaded
  8346. else { $entry->[1] = undef } # disable entry
  8347. } elsif ($entry->[1] eq \&ask_clamav ||
  8348. $entry->[0] =~ /^Mail::ClamAV/) {
  8349. if (!defined($clamav_module_ok)) {
  8350. $clamav_module_ok = eval { require Mail::ClamAV };
  8351. $clamav_module_ok = 0 if !defined $clamav_module_ok;
  8352. }
  8353. $entry->[1] = undef if !$clamav_module_ok; # disable entry
  8354. }
  8355. }
  8356. }
  8357. }
  8358. #
  8359. # Main program starts here
  8360. #
  8361. # Read dynamic source code, and logging and notification message templates
  8362. # from the end of this file (pseudo file handle DATA)
  8363. #
  8364. $Amavis::Conf::notify_spam_admin_templ = ''; # not used
  8365. $Amavis::Conf::notify_spam_recips_templ = ''; # not used
  8366. do { local($/) = "__DATA__\n"; # set line terminator to this string
  8367. chomp($_ = <Amavis::DATA>) for (
  8368. $extra_code_db, $extra_code_cache,
  8369. $extra_code_sql_base, $extra_code_sql_log, $extra_code_sql_quar,
  8370. $extra_code_sql_lookup, $extra_code_ldap,
  8371. $extra_code_in_amcl, $extra_code_in_smtp,
  8372. $extra_code_antivirus, $extra_code_antispam, $extra_code_unpackers,
  8373. $Amavis::Conf::log_templ, $Amavis::Conf::log_recip_templ);
  8374. if ($unicode_aware) {
  8375. # binmode(\*Amavis::DATA, ":encoding(utf8)") # :encoding(iso-8859-1)
  8376. # or die "Can't set \*DATA encoding: $!";
  8377. }
  8378. chomp($_ = <Amavis::DATA>) for (
  8379. $Amavis::Conf::notify_sender_templ,
  8380. $Amavis::Conf::notify_virus_sender_templ,
  8381. $Amavis::Conf::notify_virus_admin_templ,
  8382. $Amavis::Conf::notify_virus_recips_templ,
  8383. $Amavis::Conf::notify_spam_sender_templ,
  8384. $Amavis::Conf::notify_spam_admin_templ );
  8385. }; # restore line terminator
  8386. close(\*Amavis::DATA) or die "Error closing *Amavis::DATA: $!";
  8387. # close(STDIN) or die "Error closing STDIN: $!";
  8388. # note: don't close STDIN just yet to prevent some other file taking up fd 0
  8389. # discard trailing NL
  8390. $Amavis::Conf::log_templ = $1
  8391. if $Amavis::Conf::log_templ=~/^(.*?)[\r\n]+\z/s;
  8392. $Amavis::Conf::log_recip_templ = $1
  8393. if $Amavis::Conf::log_recip_templ=~/^(.*?)[\r\n]+\z/s;
  8394. # Consider droping privileges early, before reading config file.
  8395. # This is only possible if running under chroot will not be needed.
  8396. #
  8397. my($desired_group); # defaults to $desired_user's group
  8398. my($desired_user); # username or UID
  8399. if ($> != 0) { $desired_user = $> } # use effective UID if not root
  8400. #else {
  8401. # for my $u ('amavis', 'vscan') { # try to guess a good default username
  8402. # my($username,$passwd,$uid,$gid) = getpwnam($u);
  8403. # if (defined $uid && $uid != 0) { $desired_user = $u; last }
  8404. # }
  8405. #}
  8406. # collect and parse command line options
  8407. while (@ARGV >= 2 && $ARGV[0] =~ /^-[ugc]\z/) {
  8408. my($opt) = shift @ARGV;
  8409. if ($opt eq '-u') { # -u username
  8410. my($val) = shift @ARGV;
  8411. if ($> == 0) { $desired_user = $val }
  8412. else { print STDERR "Ignoring option -u when not running as root\n" }
  8413. } elsif ($opt eq '-g') { # -g group
  8414. my($val) = shift @ARGV;
  8415. if ($> == 0) { $desired_group = $val }
  8416. else { print STDERR "Ignoring option -g when not running as root\n" }
  8417. } elsif ($opt eq '-c') { # -c config_file
  8418. push(@config_files, untaint(shift @ARGV));
  8419. }
  8420. }
  8421. if (defined $desired_user && ($> == 0 || $< == 0)) { # drop privileges early
  8422. my($username,$passwd,$uid,$gid) =
  8423. $desired_user=~/^(\d+)$/ ? (undef,undef,$1,undef) :getpwnam($desired_user);
  8424. defined $uid or die "No such username: $desired_user\n";
  8425. if ($desired_group eq '') { $desired_group = $gid } # for logging purposes
  8426. else { $gid = $desired_group=~/^(\d+)$/ ? $1 : getgrnam($desired_group) }
  8427. defined $gid or die "No such group: $desired_group\n";
  8428. $( = $gid; # real GID
  8429. $) = "$gid $gid"; # effective GID
  8430. POSIX::setuid($uid) or die "Can't setuid to $uid: $!";
  8431. $> = $uid; $< = $uid; # just in case
  8432. # print STDERR "desired user=$desired_user ($uid), current: EUID: $> ($<)\n";
  8433. # print STDERR "desired group=$desired_group, current: EGID: $) ($()\n";
  8434. $> != 0 or die "Still running as root, aborting\n";
  8435. $< != 0 or die "Effective UID changed, but Real UID is 0\n";
  8436. }
  8437. umask(0027);
  8438. POSIX::setlocale(LC_TIME,"C"); # English dates required in syslog and rfc2822!
  8439. # do some remaining initialization
  8440. init_builtin_macros();
  8441. init_local_delivery_aliases();
  8442. Amavis::Conf::init_decoders();
  8443. Amavis::Conf::build_default_maps();
  8444. # default location of the config file if none specified
  8445. push(@config_files, '/etc/amavisd.conf') if !@config_files;
  8446. # Read/execute the config file, which may override default settings
  8447. Amavis::Conf::read_config(@config_files);
  8448. if (defined $desired_user && $daemon_user ne '') {
  8449. # compare the config file settings to current UID
  8450. my($username,$passwd,$uid,$gid) =
  8451. $daemon_user=~/^(\d+)$/ ? (undef,undef,$1,undef) : getpwnam($daemon_user);
  8452. $uid == $> or warn sprintf(
  8453. "WARN: running under user '%s' (UID=%s), the config file".
  8454. " specifies \$daemon_user='%s' (UID=%s)\n",
  8455. $desired_user, $>, $daemon_user, defined $uid ? $uid : '?');
  8456. }
  8457. # compile optional modules if needed
  8458. # %modules_basic = %INC; # helps to track missing modules in chroot
  8459. if (!$enable_db) { $extra_code_db = undef }
  8460. else {
  8461. eval $extra_code_db or die "Problem in Amavis::DB or Amavis::DB::SNMP code: $@";
  8462. $extra_code_db = 1; # release memory occupied by the source code
  8463. }
  8464. if (!$enable_global_cache || !$extra_code_db) { $extra_code_cache = undef }
  8465. else {
  8466. eval $extra_code_cache or die "Problem in the Amavis::Cache code: $@";
  8467. $extra_code_cache = 1; # release memory occupied by the source code
  8468. }
  8469. if (!@storage_sql_dsn) { $extra_code_sql_log = undef }
  8470. if (!@lookup_sql_dsn) { $extra_code_sql_lookup = undef }
  8471. if (!defined($extra_code_sql_log) || # sql quarantine depends on sql log
  8472. !grep { c($_)=~/^sql:/i } qw(virus_quarantine_method spam_quarantine_method
  8473. banned_files_quarantine_method bad_header_quarantine_method)
  8474. ) { $extra_code_sql_quar = undef }
  8475. if (!defined($extra_code_sql_log) && !defined($extra_code_sql_quar) &&
  8476. !defined($extra_code_sql_lookup)) { $extra_code_sql_base = undef }
  8477. else {
  8478. eval $extra_code_sql_base or die "Problem in Amavis SQL base code: $@";
  8479. $extra_code_sql_base = 1; # release memory occupied by the source code
  8480. }
  8481. if (defined $extra_code_sql_log) {
  8482. eval $extra_code_sql_log or die "Problem in Amavis::SQL::Log code: $@";
  8483. $extra_code_sql_log = 1; # release memory occupied by the source code
  8484. }
  8485. if (defined $extra_code_sql_quar) {
  8486. eval $extra_code_sql_quar or die "Problem in Amavis::SQL::Quarantine code: $@";
  8487. $extra_code_sql_quar = 1; # release memory occupied by the source code
  8488. }
  8489. if (defined $extra_code_sql_lookup) {
  8490. eval $extra_code_sql_lookup or die "Problem in Amavis SQL lookup code: $@";
  8491. $extra_code_sql_lookup = 1; # release memory occupied by the source code
  8492. }
  8493. if (!$enable_ldap) { $extra_code_ldap = undef }
  8494. else {
  8495. eval $extra_code_ldap or die "Problem in the Lookup::LDAP code: $@";
  8496. $extra_code_ldap = 1; # release memory occupied by the source code
  8497. }
  8498. { my(%needed_protocols);
  8499. for my $bank_name (keys %policy_bank) {
  8500. my($var) = $policy_bank{$bank_name}{'protocol'};
  8501. $var = $$var if ref($var) eq 'SCALAR'; # allow one level of indirection
  8502. $needed_protocols{$var} = 1 if defined $var;
  8503. }
  8504. # compatibility with older config files unaware of $protocol config variable
  8505. $needed_protocols{'AM.CL'} = 1
  8506. if defined $unix_socketname && $unix_socketname ne ''
  8507. && !grep {$needed_protocols{$_}} qw(AM.PDP COURIER);
  8508. $needed_protocols{'SMTP'} = 1
  8509. if defined $inet_socket_port && $inet_socket_port ne ''
  8510. && (!ref $inet_socket_port || @$inet_socket_port)
  8511. && !grep {$needed_protocols{$_}} qw(SMTP LMTP QMQPqq);
  8512. if ($needed_protocols{'COURIER'}) { die "In::Courier code not available" }
  8513. if ($needed_protocols{'QMQPqq'}) { die "In::QMQPqq code not available" }
  8514. if ($needed_protocols{'AM.PDP'} || $needed_protocols{'AM.CL'}) {
  8515. eval $extra_code_in_amcl or die "Problem in the In::AMCL code: $@";
  8516. $extra_code_in_amcl = 1; # release memory occupied by the source code
  8517. } else {
  8518. $extra_code_in_amcl = undef;
  8519. }
  8520. if ($needed_protocols{'SMTP'} || $needed_protocols{'LMTP'}) {
  8521. eval $extra_code_in_smtp or die "Problem in the In::SMTP code: $@";
  8522. $extra_code_in_smtp = 1; # release memory occupied by the source code
  8523. } else {
  8524. $extra_code_in_smtp = undef;
  8525. }
  8526. }
  8527. my($bpvcm) = ca('bypass_virus_checks_maps');
  8528. if (!@{ca('av_scanners')} && !@{ca('av_scanners_backup')}) {
  8529. $extra_code_antivirus = undef;
  8530. } elsif (@$bpvcm && !ref($bpvcm->[0]) && $bpvcm->[0]) {
  8531. # do a simple-minded test to make it easy to turn off virus checks
  8532. $extra_code_antivirus = undef;
  8533. } else {
  8534. eval $extra_code_antivirus or die "Problem in the antivirus code: $@";
  8535. $extra_code_antivirus = 1; # release memory occupied by the source code
  8536. }
  8537. if (!$extra_code_antivirus) # release storage
  8538. { @Amavis::Conf::av_scanners = @Amavis::Conf::av_scanners_backup = () }
  8539. my($bpscm) = ca('bypass_spam_checks_maps');
  8540. if (@$bpscm && !ref($bpscm->[0]) && $bpscm->[0]) {
  8541. # do a simple-minded test to make it easy to turn off spam checks
  8542. $extra_code_antispam = undef;
  8543. } else {
  8544. eval $extra_code_antispam or die "Problem in the antispam code: $@";
  8545. $extra_code_antispam = 1; # release memory occupied by the source code
  8546. }
  8547. if (c('bypass_decode_parts') &&
  8548. !grep {exists $policy_bank{$_}{'bypass_decode_parts'} &&
  8549. !$policy_bank{$_}{'bypass_decode_parts'} } keys %policy_bank) {
  8550. $extra_code_unpackers = undef;
  8551. } else {
  8552. eval $extra_code_unpackers or die "Problem in the Amavis::Unpackers code: $@";
  8553. $extra_code_unpackers = 1; # release memory occupied by the source code
  8554. }
  8555. # act on command line parameters
  8556. my($cmd) = lc($ARGV[0]);
  8557. if ($cmd =~ /^(start|debug|debug-sa|foreground)?\z/) {
  8558. $DEBUG=1 if $cmd eq 'debug';
  8559. $daemonize=0 if $cmd eq 'foreground';
  8560. $daemonize=0, $sa_debug='1,all' if $cmd eq 'debug-sa';
  8561. } elsif ($cmd !~ /^(reload|stop)\z/) {
  8562. die "$myversion: Unknown argument. Usage:\n $0 [-u user] [-g group] [-c config-file] ( [start] | stop | reload | debug | debug-sa | foreground )\n";
  8563. } else { # stop or reload
  8564. eval { # first stop a running daemon
  8565. $pid_file ne '' or die "Config parameter \$pid_file not defined";
  8566. my($errn) = stat($pid_file) ? 0 : 0+$!;
  8567. $errn != ENOENT or die "No PID file $pid_file\n";
  8568. $errn == 0 or die "PID file $pid_file inaccessible: $!";
  8569. my($amavisd_pid); local(*PID_FILE); my($ln);
  8570. open(PID_FILE, "< $pid_file\0") or die "Can't open file $pid_file: $!";
  8571. for (undef $!; defined($ln=<PID_FILE>); undef $!)
  8572. { chomp($ln); $amavisd_pid = $ln if $ln =~ /^\d+\z/ }
  8573. defined $ln || $!==0 or die "Error reading from $pid_file: $!";
  8574. close(PID_FILE) or die "Error closing file $pid_file: $!";
  8575. defined($amavisd_pid) or die "Invalid PID in the $pid_file";
  8576. $amavisd_pid = untaint($amavisd_pid);
  8577. kill('TERM',$amavisd_pid) or die "Can't SIGTERM amavisd[$amavisd_pid]: $!";
  8578. my($waited) = 0; my($sigkill_sent) = 0; my($delay) = 1; # seconds
  8579. for (;;) { # wait for the old running daemon to go away
  8580. sleep($delay); $waited += $delay; $delay = 5;
  8581. last if !kill(0,$amavisd_pid); # is the old daemon still there?
  8582. if ($waited < 60 || $sigkill_sent) {
  8583. print STDERR "Waiting for the process $amavisd_pid to terminate\n";
  8584. } else { # use stronger hammer
  8585. print STDERR "Sending SIGKILL to amavisd[$amavisd_pid]\n";
  8586. kill('KILL',$amavisd_pid)
  8587. or warn "Can't SIGKILL amavisd[$amavisd_pid]: $!";
  8588. $sigkill_sent = 1;
  8589. }
  8590. }
  8591. };
  8592. if ($@ ne '') { chomp($@); die "$@, can't $cmd the process\n" }
  8593. exit 0 if $cmd eq 'stop';
  8594. print STDERR "daemon terminated, waiting for the dust to settle...\n";
  8595. sleep 5; # wait for the TCP socket to be released
  8596. print STDERR "becoming a new daemon...\n";
  8597. }
  8598. $daemonize = 0 if $DEBUG;
  8599. # Set path, home and term explictly. Don't trust environment
  8600. $ENV{PATH} = $path if $path ne '';
  8601. $ENV{HOME} = $helpers_home if $helpers_home ne '';
  8602. $ENV{TERM} = 'dumb'; $ENV{COLUMNS} = '80'; $ENV{LINES} = '100';
  8603. Amavis::Log::init($DEBUG, $DO_SYSLOG, $SYSLOG_LEVEL, $LOGFILE);
  8604. # report version of Perl and process UID
  8605. do_log(1, "user=$desired_user, EUID: $> ($<); group=$desired_group, EGID: $) ($()");
  8606. do_log(0, "Perl version $]");
  8607. # insist on a FQDN in $myhostname
  8608. $myhostname =~ /[^.]\.[a-zA-Z0-9]+\z/s || lc($myhostname) eq 'localhost'
  8609. or die <<"EOD";
  8610. The value of variable \$myhostname is \"$myhostname\", but should have been
  8611. a fully qualified domain name; perhaps uname(3) did not provide such.
  8612. You must explicitly assign a FQDN of this host to variable \$myhostname
  8613. in amavisd.conf, or fix what uname(3) provides as a host's network name!
  8614. EOD
  8615. # $SIG{USR2} = sub {
  8616. # my($msg) = Carp::longmess("SIG$_[0] received, backtrace:");
  8617. # print STDERR "\n",$msg,"\n"; do_log(-1,$msg);
  8618. # };
  8619. # pre-parse IP lookup tables to speed up lookups
  8620. for my $bank_name (keys %policy_bank) {
  8621. my($r) = $policy_bank{$bank_name}{'inet_acl'};
  8622. if (ref($r) eq 'ARRAY') # should be a ref to single IP lookup table
  8623. { $policy_bank{$bank_name}{'inet_acl'} = Amavis::Lookup::IP->new(@$r) }
  8624. $r = $policy_bank{$bank_name}{'mynetworks_maps'}; # ref to list of tables
  8625. if (ref($r) eq 'ARRAY') { # should be an array, test just to make sure
  8626. for my $table (@$r) # replace plain lists with Amavis::Lookup::IP objects
  8627. { $table = Amavis::Lookup::IP->new(@$table) if ref($table) eq 'ARRAY' }
  8628. }
  8629. }
  8630. fetch_modules_extra(); # bring additional modules into memory and compile them
  8631. # set up Net::Server configuration
  8632. my $server = bless {
  8633. server => {
  8634. # command args to be used after HUP must be untainted, deflt: [$0,@ARGV]
  8635. # commandline => ['/usr/local/sbin/amavisd','-c',$config_file[0] ],
  8636. commandline => [], # disable
  8637. # listen on the following sockets (one or more):
  8638. port => [ (!defined($unix_socketname) || $unix_socketname eq '' ? ()
  8639. : "$unix_socketname|unix"), # helper
  8640. map { "$_/tcp" } # accept SMTP on this port(s)
  8641. (ref $inet_socket_port ? @$inet_socket_port
  8642. : $inet_socket_port ne '' ? $inet_socket_port : () ),
  8643. ],
  8644. # limit socket bind (e.g. to the loopback interface)
  8645. host => (!defined($inet_socket_bind) || $inet_socket_bind eq '' ? '*'
  8646. : $inet_socket_bind),
  8647. max_servers => $max_servers, # number of pre-forked children
  8648. max_requests => $max_requests, # restart child after that many accept's
  8649. user => (($> == 0 || $< == 0) ? $daemon_user : undef),
  8650. group => (($> == 0 || $< == 0) ? $daemon_group : undef),
  8651. pid_file => $pid_file,
  8652. lock_file => $lock_file, # serialization lockfile
  8653. # serialize => 'flock', # flock, semaphore, pipe
  8654. background => $daemonize ? 1 : undef,
  8655. setsid => $daemonize ? 1 : undef,
  8656. chroot => $daemon_chroot_dir ne '' ? $daemon_chroot_dir : undef,
  8657. no_close_by_child => 1,
  8658. # controls log level for Net::Server internal log messages:
  8659. # 0=err, 1=warning, 2=notice, 3=info, 4=debug
  8660. log_level => ($DEBUG ? 4 : 2),
  8661. log_file => undef, # will be overridden to call do_log()
  8662. },
  8663. }, 'Amavis';
  8664. $0 = 'amavisd (master)';
  8665. $server->run; # transfer control to Net::Server
  8666. # shouldn't get here
  8667. exit 1;
  8668. # we read text (especially notification templates) from DATA sections
  8669. # to avoid any interpretations of special characters (e.g. \ or ') by Perl
  8670. #
  8671. __DATA__
  8672. #
  8673. package Amavis::DB::SNMP;
  8674. use strict;
  8675. use re 'taint';
  8676. BEGIN {
  8677. import Amavis::Conf qw($myversion $myhostname);
  8678. import Amavis::Util qw(ll do_log snmp_counters_get
  8679. add_entropy fetch_entropy);
  8680. }
  8681. use BerkeleyDB;
  8682. BEGIN {
  8683. use Exporter ();
  8684. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  8685. $VERSION = '2.043';
  8686. @ISA = qw(Exporter);
  8687. }
  8688. # open existing databases (called by each child process)
  8689. sub new {
  8690. my($class,$db_env) = @_; undef $!; my($env) = $db_env->get_db_env;
  8691. defined $env or die "BDB bad db env.: $BerkeleyDB::Error, $!.";
  8692. undef $!; my($dbs) = BerkeleyDB::Hash->new(-Filename=>'snmp.db', -Env=>$env);
  8693. defined $dbs or die "BDB no dbS: $BerkeleyDB::Error, $!.";
  8694. undef $!; my($dbn) = BerkeleyDB::Hash->new(-Filename=>'nanny.db',-Env=>$env);
  8695. defined $dbn or die "BDB no dbN: $BerkeleyDB::Error, $!.";
  8696. bless { 'db_snmp'=>$dbs, 'db_nanny'=>$dbn }, $class;
  8697. }
  8698. sub DESTROY {
  8699. my($self) = shift;
  8700. eval { do_log(5,"Amavis::DB::SNMP DESTROY called") };
  8701. for my $db ($self->{'db_snmp'}, $self->{'db_nanny'}) {
  8702. if (defined $db) {
  8703. eval { $db->db_close==0 or die "db_close: $BerkeleyDB::Error, $!." };
  8704. if ($@ ne '') { warn "BDB S+N DESTROY $@" }
  8705. $db = undef;
  8706. }
  8707. }
  8708. }
  8709. #sub lock_stat($) {
  8710. # my($label) = @_;
  8711. # my($s) = qx'/usr/local/bin/db_stat-4.2 -c -h /var/amavis/db | /usr/local/bin/perl -ne \'$a{$2}=$1 if /^(\d+)\s+Total number of locks (requested|released)/; END {printf("%d, %d\n",$a{requested}, $a{requested}-$a{released})}\'';
  8712. # do_log(0, "lock_stat $label: $s");
  8713. #}
  8714. # insert startup time SNMP entry, called from the master process at startup
  8715. # (a classical subroutine, not a method)
  8716. sub put_initial_snmp_data($) {
  8717. my($db) = @_;
  8718. my($cursor) = $db->db_cursor(DB_WRITECURSOR);
  8719. defined $cursor or die "BDB S db_cursor: $BerkeleyDB::Error, $!.";
  8720. for my $obj (['sysDescr', 'STR', $myversion],
  8721. ['sysObjectID', 'OID', '1.3.6.1.4.1.15312.2.1'],
  8722. # iso.org.dod.internet.private.enterprise.ijs.amavisd-new.snmp
  8723. ['sysUpTime', 'INT', int(time)],
  8724. # later it must be converted to timeticks (10ms since start)
  8725. ['sysContact', 'STR', ''],
  8726. ['sysName', 'STR', $myhostname],
  8727. ['sysLocation', 'STR', ''],
  8728. ['sysServices', 'INT', 64], # application
  8729. ) {
  8730. my($key,$type,$val) = @$obj;
  8731. $cursor->c_put($key, sprintf("%s %s",$type,$val), DB_KEYLAST) == 0
  8732. or die "BDB S c_put: $BerkeleyDB::Error, $!.";
  8733. };
  8734. $cursor->c_close==0 or die "BDB S c_close: $BerkeleyDB::Error, $!.";
  8735. }
  8736. sub update_snmp_variables {
  8737. my($self) = @_;
  8738. do_log(5,"updating snmp variables");
  8739. my($snmp_var_names_ref) = snmp_counters_get();
  8740. my($eval_stat,$interrupt); $interrupt = '';
  8741. if (defined $snmp_var_names_ref && @$snmp_var_names_ref) {
  8742. my($db) = $self->{'db_snmp'}; my($cursor);
  8743. my($h1) = sub { $interrupt = $_[0] };
  8744. local(@SIG{qw(INT HUP TERM TSTP QUIT ALRM USR1 USR2)}) = ($h1) x 8;
  8745. eval { # ensure cursor will be unlocked even in case of errors or signals
  8746. $cursor = $db->db_cursor(DB_WRITECURSOR); # obtain write lock
  8747. defined $cursor or die "db_cursor: $BerkeleyDB::Error, $!.";
  8748. for my $key (@$snmp_var_names_ref) {
  8749. my($snmp_var_name,$arg,$type) = ref $key ? @$key : ($key);
  8750. $type = 'C32' if !defined($type) || $type eq '';
  8751. $arg = 1 if !defined($arg) && $type eq 'C32';
  8752. my($val,$flags);
  8753. my($stat) = $cursor->c_get($snmp_var_name,$val,DB_SET);
  8754. if ($stat==0) { # exists, update it
  8755. if ($type eq 'C32' && $val=~/^C32 (\d+)\z/) { $val = $1+$arg }
  8756. elsif ($type eq 'INT' && $val=~/^INT (\d+)\z/) { $val = $arg }
  8757. elsif ($type=~/^(STR|OID)\z/ && $val=~/^\Q$type\E (.*)\z/) {
  8758. if ($snmp_var_name ne 'entropy') { $val = $arg }
  8759. else { # blend-in entropy
  8760. $val = $1; add_entropy($val);
  8761. $val = substr(fetch_entropy(),-10,10); # save only 60 tail bits
  8762. }
  8763. }
  8764. else { do_log(-2,"WARN: variable syntax? $val, clearing"); $val = 0 }
  8765. $flags = DB_CURRENT;
  8766. } else { # create new entry
  8767. $stat==DB_NOTFOUND or die "c_get: $BerkeleyDB::Error, $!.";
  8768. $flags = DB_KEYLAST; $val = $arg;
  8769. }
  8770. my($str) = $type =~ /^(C32|INT)\z/ ? sprintf("%010d",$val) : $val;
  8771. $cursor->c_put($snmp_var_name, "$type $str", $flags) == 0
  8772. or die "c_put: $BerkeleyDB::Error, $!.";
  8773. }
  8774. $cursor->c_close==0 or die "c_close: $BerkeleyDB::Error, $!.";
  8775. $cursor = undef;
  8776. };
  8777. $eval_stat = $@;
  8778. if (defined $db) {
  8779. $cursor->c_close if defined $cursor; # unlock, ignoring status
  8780. $cursor = undef;
  8781. # if ($eval_stat eq '') {
  8782. # my($stat); $db->db_sync(); # not really needed
  8783. # $stat==0 or warn "BDB S db_sync, status $stat: $BerkeleyDB::Error, $!.";
  8784. # }
  8785. }
  8786. }
  8787. delete $self->{'cnt'};
  8788. if ($interrupt ne '') { kill($interrupt,$$) } # resignal
  8789. elsif ($eval_stat ne '')
  8790. { chomp($eval_stat); die "update_snmp_variables: BDB S $eval_stat\n" }
  8791. }
  8792. sub read_snmp_variables {
  8793. my($self,@snmp_var_names) = @_;
  8794. my($eval_stat,$interrupt); $interrupt = '';
  8795. my($db) = $self->{'db_snmp'}; my($cursor); my(@values);
  8796. my($h1) = sub { $interrupt = $_[0] };
  8797. local(@SIG{qw(INT HUP TERM TSTP QUIT ALRM USR1 USR2)}) = ($h1) x 8;
  8798. eval { # ensure cursor will be unlocked even in case of errors or signals
  8799. $cursor = $db->db_cursor; # obtain read lock
  8800. defined $cursor or die "db_cursor: $BerkeleyDB::Error, $!.";
  8801. for my $cname (@snmp_var_names) {
  8802. my($val); my($stat) = $cursor->c_get($cname,$val,DB_SET);
  8803. push(@values, $stat==0 ? $val : undef);
  8804. $stat==0 || $stat==DB_NOTFOUND or die "c_get: $BerkeleyDB::Error, $!.";
  8805. }
  8806. $cursor->c_close==0 or die "c_close: $BerkeleyDB::Error, $!.";
  8807. $cursor = undef;
  8808. };
  8809. $eval_stat = $@;
  8810. if (defined $db) {
  8811. $cursor->c_close if defined $cursor; # unlock, ignoring status
  8812. $cursor = undef;
  8813. }
  8814. if ($interrupt ne '') { kill($interrupt,$$) } # resignal
  8815. elsif ($eval_stat ne '')
  8816. { chomp($eval_stat); die "read_snmp_variables: BDB S $eval_stat\n" }
  8817. for my $val (@values) {
  8818. if (!defined($val)) {} # keep undefined
  8819. elsif ($val =~ /^(?:C32|INT) (\d+)\z/) { $val = 0+$1 }
  8820. elsif ($val =~ /^(?:STR|OID) (.*)\z/) { $val = $1 }
  8821. else { do_log(-2,"WARN: counter syntax? $val"); $val = undef }
  8822. }
  8823. \@values;
  8824. }
  8825. sub register_proc {
  8826. my($self,$task_id) = @_;
  8827. my($db) = $self->{'db_nanny'}; my($cursor);
  8828. my($val,$new_val); my($key) = sprintf("%05d",$$);
  8829. $new_val = sprintf("%010d %-12s", time, $task_id) if defined $task_id;
  8830. my($eval_stat,$interrupt); $interrupt = '';
  8831. my($h1) = sub { $interrupt = $_[0] };
  8832. local(@SIG{qw(INT HUP TERM TSTP QUIT ALRM USR1 USR2)}) = ($h1) x 8;
  8833. eval { # ensure cursor will be unlocked even in case of errors or signals
  8834. $cursor = $db->db_cursor(DB_WRITECURSOR); # obtain write lock
  8835. defined $cursor or die "db_cursor: $BerkeleyDB::Error, $!.";
  8836. my($stat) = $cursor->c_get($key,$val,DB_SET);
  8837. $stat==0 || $stat==DB_NOTFOUND or die "c_get: $BerkeleyDB::Error, $!.";
  8838. if ($stat==0 && !defined $task_id) { # remove existing entry
  8839. $cursor->c_del==0 or die "c_del: $BerkeleyDB::Error, $!.";
  8840. } elsif (defined $task_id && !($stat==0 && $new_val eq $val)) {
  8841. # add new, or update existing entry if different
  8842. $cursor->c_put($key, $new_val,
  8843. $stat==0 ? DB_CURRENT : DB_KEYLAST ) == 0
  8844. or die "c_put: $BerkeleyDB::Error, $!.";
  8845. }
  8846. $cursor->c_close==0 or die "c_close: $BerkeleyDB::Error, $!.";
  8847. $cursor = undef;
  8848. };
  8849. $eval_stat = $@;
  8850. if (defined $db) {
  8851. $cursor->c_close if defined $cursor; # unlock, ignoring status
  8852. $cursor = undef;
  8853. # if ($eval_stat eq '') {
  8854. # my($stat) = $db->db_sync(); # not really needed
  8855. # $stat==0 or warn "BDB N db_sync, status $stat: $BerkeleyDB::Error, $!.";
  8856. # }
  8857. }
  8858. if ($interrupt ne '') { kill($interrupt,$$) } # resignal
  8859. elsif ($eval_stat ne '')
  8860. { chomp($eval_stat); die "register_proc: BDB N $eval_stat\n" }
  8861. }
  8862. 1;
  8863. #
  8864. package Amavis::DB;
  8865. use strict;
  8866. use re 'taint';
  8867. BEGIN {
  8868. import Amavis::Conf qw($db_home $daemon_chroot_dir);
  8869. import Amavis::Util qw(untaint ll do_log);
  8870. }
  8871. use BerkeleyDB;
  8872. BEGIN {
  8873. use Exporter ();
  8874. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  8875. $VERSION = '2.043';
  8876. @ISA = qw(Exporter);
  8877. }
  8878. # create new databases, then close them (called by the parent process)
  8879. # (called only if $db_home is nonempty)
  8880. sub init($) {
  8881. my($predelete) = @_; # delete existing db files first?
  8882. my($name) = $db_home;
  8883. $name = "$daemon_chroot_dir $name" if $daemon_chroot_dir ne '';
  8884. if ($predelete) { # delete old database files
  8885. local(*DIR);
  8886. opendir(DIR,$db_home) or die "db_init: Can't open directory $name: $!";
  8887. my(@dirfiles) = readdir(DIR); #must avoid modifying dir while traversing it
  8888. closedir(DIR) or die "db_init: Error closing directory $name: $!";
  8889. for my $f (@dirfiles) {
  8890. next if ($f eq '.' || $f eq '..') && -d _;
  8891. if ($f =~ /^(__db\.\d+|(cache-expiry|cache|snmp|nanny)\.db)\z/s) {
  8892. $f = untaint($f);
  8893. unlink("$db_home/$f") or die "db_init: Can't delete file $name/$f: $!";
  8894. }
  8895. }
  8896. }
  8897. undef $!; my($env) = BerkeleyDB::Env->new(-Home=>$db_home, -Mode=>0640,
  8898. -Flags=> DB_CREATE | DB_INIT_CDB | DB_INIT_MPOOL);
  8899. defined $env
  8900. or die "db_init: BDB bad db env. at $db_home: $BerkeleyDB::Error, $!.";
  8901. do_log(0, sprintf("Creating db in %s/; BerkeleyDB %s, libdb %s",
  8902. $name, BerkeleyDB->VERSION, $BerkeleyDB::db_version));
  8903. undef $!; my($dbc) = BerkeleyDB::Hash->new(
  8904. -Filename=>'cache.db', -Flags=>DB_CREATE, -Env=>$env );
  8905. defined $dbc or die "db_init: BDB no dbC: $BerkeleyDB::Error, $!.";
  8906. undef $!; my($dbq) = BerkeleyDB::Queue->new(
  8907. -Filename=>'cache-expiry.db', -Flags=>DB_CREATE, -Env=>$env,
  8908. -Len=>15+1+32 ); # '-ExtentSize' needs DB 3.2.x, e.g. -ExtentSize=>2
  8909. defined $dbq or die "db_init: BDB no dbQ: $BerkeleyDB::Error, $!.";
  8910. undef $!; my($dbs) = BerkeleyDB::Hash->new(
  8911. -Filename=>'snmp.db', -Flags=>DB_CREATE, -Env=>$env );
  8912. defined $dbs or die "db_init: BDB no dbS: $BerkeleyDB::Error, $!.";
  8913. undef $!; my($dbn) = BerkeleyDB::Hash->new(
  8914. -Filename=>'nanny.db', -Flags=>DB_CREATE, -Env=>$env );
  8915. defined $dbn or die "db_init: BDB no dbN: $BerkeleyDB::Error, $!.";
  8916. Amavis::DB::SNMP::put_initial_snmp_data($dbs);
  8917. for my $db ($dbc, $dbq, $dbs, $dbn) {
  8918. $db->db_close==0 or die "db_init: BDB db_close: $BerkeleyDB::Error, $!.";
  8919. }
  8920. }
  8921. # open an existing databases environment (called by each child process)
  8922. sub new {
  8923. my($class) = @_; my($env);
  8924. if (defined $db_home) {
  8925. $env = BerkeleyDB::Env->new(
  8926. -Home=>$db_home, -Mode=>0640, -Flags=> DB_INIT_CDB | DB_INIT_MPOOL);
  8927. defined $env or die "BDB bad db env. at $db_home: $BerkeleyDB::Error, $!.";
  8928. }
  8929. bless \$env, $class;
  8930. }
  8931. sub get_db_env { my($self) = shift; $$self }
  8932. 1;
  8933. __DATA__
  8934. #
  8935. package Amavis::Cache;
  8936. # offer an 'IPC::Cache'-compatible interface to a BerkeleyDB-based cache.
  8937. # Replaces methods new,get,set of the memory-based cache.
  8938. use strict;
  8939. use re 'taint';
  8940. BEGIN {
  8941. import Amavis::Util qw(ll do_log);
  8942. }
  8943. use BerkeleyDB;
  8944. BEGIN {
  8945. use Exporter ();
  8946. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  8947. $VERSION = '2.0432';
  8948. @ISA = qw(Exporter);
  8949. }
  8950. # open existing databases (called by each child process);
  8951. # if $db_env is undef a memory-based cache is created, otherwise use BerkeleyDB
  8952. sub new {
  8953. my($class,$db_env) = @_;
  8954. my($dbc,$dbq,$mem_cache);
  8955. if (!defined($db_env)) {
  8956. do_log(1,"BerkeleyDB not available, using memory-based local cache");
  8957. $mem_cache = {};
  8958. } else {
  8959. my($env) = $db_env->get_db_env;
  8960. defined $env or die "BDB bad db env.: $BerkeleyDB::Error, $!.";
  8961. $dbc = BerkeleyDB::Hash->new(-Filename=>'cache.db', -Env=>$env);
  8962. defined $dbc or die "BDB no dbC: $BerkeleyDB::Error, $!.";
  8963. $dbq = BerkeleyDB::Queue->new(-Filename=>'cache-expiry.db', -Env=>$env,
  8964. -Len=>15+1+32); # '-ExtentSize' needs DB 3.2.x, e.g. -ExtentSize=>2
  8965. defined $dbq or die "BDB no dbQ: $BerkeleyDB::Error, $!.";
  8966. }
  8967. bless {'db_cache'=>$dbc, 'db_queue'=>$dbq, 'mem_cache'=>$mem_cache}, $class;
  8968. }
  8969. sub DESTROY {
  8970. my($self) = shift;
  8971. eval { do_log(5,"Amavis::Cache DESTROY called") };
  8972. for my $db ($self->{'db_cache'}, $self->{'db_queue'}) {
  8973. if (defined $db) {
  8974. eval { $db->db_close==0 or die "db_close: $BerkeleyDB::Error, $!." };
  8975. if ($@ ne '') { warn "BDB C+Q DESTROY $@" }
  8976. $db = undef;
  8977. }
  8978. }
  8979. }
  8980. # purge expired entries from the queue head and enqueue new entry at the tail
  8981. sub enqueue {
  8982. my($self,$str,$now_utc_iso8601,$expires_utc_iso8601) = @_;
  8983. my($db) = $self->{'db_cache'}; my($dbq) = $self->{'db_queue'};
  8984. local($1,$2); my($stat,$key,$val); $key = '';
  8985. my($qcursor) = $dbq->db_cursor(DB_WRITECURSOR);
  8986. defined $qcursor or die "BDB Q db_cursor: $BerkeleyDB::Error, $!.";
  8987. # no warnings 'numeric'; # seems like c_get can return an empty string?!
  8988. while ( $stat=$qcursor->c_get($key,$val,DB_NEXT), $stat eq '' || $stat==0 ) {
  8989. do_log(5,"enqueue: stat is not numeric: \"$stat\"") if $stat !~ /^\d+\z/;
  8990. if ($val !~ /^([^ ]+) (.*)\z/s) {
  8991. do_log(-2,"WARN: queue head invalid, deleting: $val");
  8992. } else {
  8993. my($t,$digest) = ($1,$2);
  8994. last if $t ge $now_utc_iso8601;
  8995. my($cursor) = $db->db_cursor(DB_WRITECURSOR);
  8996. defined $cursor or die "BDB C db_cursor: $BerkeleyDB::Error, $!.";
  8997. my($v); my($st1) = $cursor->c_get($digest,$v,DB_SET);
  8998. $st1==0 || $st1==DB_NOTFOUND or die "BDB C c_get: $BerkeleyDB::Error, $!.";
  8999. if ($st1==0 && $v=~/^([^ ]+) /s) { # record exists and appears valid
  9000. if ($1 ne $t) {
  9001. do_log(5,"enqueue: not deleting: $digest, was refreshed since");
  9002. } else { # its expiration time correspond to timestamp in the queue
  9003. do_log(5,"enqueue: deleting: $digest");
  9004. my($st2) = $cursor->c_del; # delete expired entry from the cache
  9005. $st2==0 || $st2==DB_KEYEMPTY
  9006. or die "BDB C c_del: $BerkeleyDB::Error, $!.";
  9007. }
  9008. }
  9009. $cursor->c_close==0 or die "BDB C c_close: $BerkeleyDB::Error, $!.";
  9010. }
  9011. my($st3) = $qcursor->c_del;
  9012. $st3==0 || $st3==DB_KEYEMPTY or die "BDB Q c_del: $BerkeleyDB::Error, $!.";
  9013. }
  9014. $stat==0 || $stat==DB_NOTFOUND or die "BDB Q c_get: $BerkeleyDB::Error, $!.";
  9015. $qcursor->c_close==0 or die "BDB Q c_close: $BerkeleyDB::Error, $!.";
  9016. # insert new expiration request in the queue
  9017. $dbq->db_put($key, "$expires_utc_iso8601 $str", DB_APPEND) == 0
  9018. or die "BDB Q db_put: $BerkeleyDB::Error, $!.";
  9019. # syncing would only be worth doing if we would want the cache to persist
  9020. # across restarts - but we scratch the databases to avoid rebuild worries
  9021. # $stat = $dbq->db_sync();
  9022. # $stat==0 or warn "BDB Q db_sync, status $stat: $BerkeleyDB::Error, $!.";
  9023. # $stat = $db->db_sync();
  9024. # $stat==0 or warn "BDB C db_sync, status $stat: $BerkeleyDB::Error, $!.";
  9025. }
  9026. sub get {
  9027. my($self,$key) = @_;
  9028. my($val); my($db) = $self->{'db_cache'};
  9029. if (!defined($db)) {
  9030. $val = $self->{'mem_cache'}{$key}; # simple local memory-based cache
  9031. } else {
  9032. my($stat) = $db->db_get($key,$val);
  9033. $stat==0 || $stat==DB_NOTFOUND
  9034. or die "BDB C c_get: $BerkeleyDB::Error, $!.";
  9035. local($1,$2);
  9036. if ($stat==0 && $val=~/^([^ ]+) (.*)/s) { $val = $2 } else { $val = undef }
  9037. }
  9038. thaw($val);
  9039. }
  9040. sub set {
  9041. my($self,$key,$obj,$now_utc_iso8601,$expires_utc_iso8601) = @_;
  9042. my($db) = $self->{'db_cache'};
  9043. if (!defined($db)) {
  9044. $self->{'mem_cache'}{$key} = freeze($obj);
  9045. } else {
  9046. my($cursor) = $db->db_cursor(DB_WRITECURSOR);
  9047. defined $cursor or die "BDB C db_cursor: $BerkeleyDB::Error, $!.";
  9048. my($val); my($stat) = $cursor->c_get($key,$val,DB_SET);
  9049. $stat==0 || $stat==DB_NOTFOUND
  9050. or die "BDB C c_get: $BerkeleyDB::Error, $!.";
  9051. $cursor->c_put($key, $expires_utc_iso8601.' '.freeze($obj),
  9052. $stat==0 ? DB_CURRENT : DB_KEYLAST ) == 0
  9053. or die "BDB C c_put: $BerkeleyDB::Error, $!.";
  9054. $cursor->c_close==0 or die "BDB C c_close: $BerkeleyDB::Error, $!.";
  9055. # $stat = $db->db_sync(); # only worth doing if cache were persistent
  9056. # $stat==0 or warn "BDB C db_sync, status $stat: $BerkeleyDB::Error, $!.";
  9057. $self->enqueue($key,$now_utc_iso8601,$expires_utc_iso8601);
  9058. }
  9059. $obj;
  9060. }
  9061. 1;
  9062. __DATA__
  9063. #^L
  9064. package Amavis::Out::SQL::Connection;
  9065. use strict;
  9066. use re 'taint';
  9067. BEGIN {
  9068. use Exporter ();
  9069. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  9070. $VERSION = '2.043';
  9071. @ISA = qw(Exporter);
  9072. }
  9073. use DBI;
  9074. BEGIN {
  9075. import Amavis::Conf qw(c cr ca);
  9076. import Amavis::Util qw(ll do_log);
  9077. import Amavis::Timing qw(section_time);
  9078. }
  9079. # one object per connection (normally exactly one) to a database server;
  9080. # connection need not exist at all times, stores info on how to connect;
  9081. # when connected it holds database handle
  9082. sub new {
  9083. my($class, @dsns) = @_; # a list of DSNs to try connecting to sequentially
  9084. bless { dbh=>undef, sth=>undef, incarnation=>1, dsn_list=>\@dsns }, $class;
  9085. }
  9086. sub dbh { # get/set database handle
  9087. my($self)=shift; !@_ ? $self->{dbh} : ($self->{dbh}=shift);
  9088. }
  9089. sub sth { # get/set statement handle
  9090. my($self)=shift; my($clause)=shift;
  9091. !@_ ? $self->{sth}{$clause} : ($self->{sth}{$clause}=shift);
  9092. }
  9093. sub dbh_inactive { # get/set dbh "InactiveDestroy" attribute
  9094. my($self)=shift; my($dbh) = $self->dbh;
  9095. if (!$dbh) { undef }
  9096. else { !@_ ? $dbh->{'InactiveDestroy'} : ($dbh->{'InactiveDestroy'}=shift) }
  9097. }
  9098. sub DESTROY {
  9099. my($self) = shift;
  9100. eval { do_log(5,"Amavis::Out::SQL::Connection DESTROY called") };
  9101. eval { $self->disconnect_from_sql };
  9102. }
  9103. # returns current connection version; works like cache versioning/invalidation:
  9104. # SQL statement handles need to rebuilt and caches cleared when SQL connection
  9105. # is re-established and a new database handle provided
  9106. #
  9107. sub incarnation { my($self)=shift; $self->{incarnation} }
  9108. # DBI method wrappers:
  9109. sub begin_work {
  9110. my($self)=shift; do_log(5,"sql begin transaction");
  9111. # DBD::mysql man page: if you detect an error while changing
  9112. # the AutoCommit mode, you should no longer use the database handle.
  9113. # In other words, you should disconnect and reconnect again
  9114. $self->dbh or $self->connect_to_sql;
  9115. eval { $self->dbh->begin_work(@_) };
  9116. if ($@ ne '') {
  9117. chomp($@); do_log(-1,"sql begin transaction failed, ".
  9118. "probably disconnected by server, reconnecting ($@)");
  9119. $self->disconnect_from_sql; $self->connect_to_sql;
  9120. $self->dbh->begin_work(@_);
  9121. }
  9122. $self->{in_transaction} = 1;
  9123. };
  9124. sub begin_work_nontransaction {
  9125. my($self)=shift; do_log(5,"sql begin, nontransaction");
  9126. $self->dbh or $self->connect_to_sql;
  9127. };
  9128. sub commit {
  9129. my($self)=shift; do_log(5,"sql commit");
  9130. $self->{in_transaction} = 0;
  9131. $self->dbh or die "commit: dbh not available";
  9132. $self->dbh->commit(@_);
  9133. };
  9134. sub rollback {
  9135. my($self)=shift; do_log(5,"sql rollback");
  9136. $self->{in_transaction} = 0;
  9137. $self->dbh or die "rollback: dbh not available";
  9138. eval { $self->dbh->rollback(@_) };
  9139. if ($@ ne '') {
  9140. chomp($@); do_log(-1,"sql rollback error, reconnecting ($@)");
  9141. $self->disconnect_from_sql; $self->connect_to_sql;
  9142. # $self->dbh->rollback(@_); # too late now, hopefully implied in disconnect
  9143. }
  9144. };
  9145. sub last_insert_id {
  9146. my($self)=shift;
  9147. $self->dbh or die "last_insert_id: dbh not available";
  9148. $self->dbh->last_insert_id(@_);
  9149. };
  9150. sub fetchrow_arrayref {
  9151. my($self,$clause,@args) = @_;
  9152. $self->dbh or die "fetchrow_arrayref: dbh not available";
  9153. $self->sth($clause) or die "fetchrow_arrayref: sth not available";
  9154. $self->sth($clause)->fetchrow_arrayref(@args);
  9155. };
  9156. sub finish {
  9157. my($self,$clause,@args) = @_;
  9158. $self->dbh or die "finish: dbh not available";
  9159. $self->sth($clause) or die "finish: sth not available";
  9160. $self->sth($clause)->finish(@args);
  9161. };
  9162. sub execute {
  9163. my($self,$clause,@args) = @_;
  9164. $self->dbh or die "execute: dbh not available";
  9165. my($sth) = $self->sth($clause); # fetch cached st. handle or prepare new
  9166. if ($sth) {
  9167. do_log(5,"sql: executing clause: $clause");
  9168. } else {
  9169. do_log(4,"sql: preparing and executing: $clause");
  9170. $sth = $self->dbh->prepare($clause); $self->sth($clause,$sth);
  9171. }
  9172. eval { $sth->execute(@args) };
  9173. if ($@ ne '') {
  9174. my($err) = $@; chomp($err); my($msg) = "sql execute: sts=$DBI::err, $err";
  9175. if (!$sth || ($sth->err ne '2006' && $sth->err ne '2013')) {
  9176. die $msg;
  9177. } else { # MySQL specific: server has gone away; Lost connection to...
  9178. if ($self->{in_transaction}) {
  9179. $self->disconnect_from_sql;
  9180. die "sql execute failed within transaction, $msg";
  9181. } else { # try one more time
  9182. do_log(0,"NOTICE: reconnecting in response to: $msg");
  9183. $self->disconnect_from_sql;
  9184. $self->connect_to_sql;
  9185. $self->dbh or die "execute: reconnect failed";
  9186. do_log(4,"sql: preparing and executing (again): $clause");
  9187. $sth = $self->dbh->prepare($clause); $self->sth($clause,$sth);
  9188. eval { $sth->execute(@args) };
  9189. if ($@ ne '') {
  9190. $err = $@; chomp($err); $msg = "sql execute: sts=$DBI::err, $err";
  9191. $self->disconnect_from_sql;
  9192. die "failed again, $msg";
  9193. }
  9194. }
  9195. }
  9196. }
  9197. 1;
  9198. }
  9199. # Connect to a database. Take a list of database connection
  9200. # parameters and try each until one succeeds.
  9201. # -- based on code from Ben Ransford <amavis@uce.ransford.org> 2002-09-22
  9202. sub connect_to_sql {
  9203. my($self) = shift; # a list of DSNs to try connecting to sequentially
  9204. my($dbh); my(@dsns) = @{$self->{dsn_list}};
  9205. do_log(3,"Connecting to SQL database server");
  9206. for my $tmpdsn (@dsns) {
  9207. my($dsn, $username, $password) = @$tmpdsn;
  9208. do_log(4,"connect_to_sql: trying '$dsn'");
  9209. $dbh = DBI->connect($dsn, $username, $password,
  9210. {PrintError => 0, RaiseError => 0, Taint => 1, AutoCommit => 1} );
  9211. if ($dbh) { do_log(3,"connect_to_sql: '$dsn' succeeded"); last }
  9212. do_log(-1,"connect_to_sql: unable to connect to DSN '$dsn': ".$DBI::errstr);
  9213. }
  9214. $self->dbh($dbh); delete($self->{sth});
  9215. $self->{in_transaction} = 0; $self->{incarnation}++;
  9216. $dbh or die "connect_to_sql: unable to connect to any dataset";
  9217. $dbh->{'RaiseError'} = 1;
  9218. # $dbh->{mysql_auto_reconnect} = 1; # questionable benefit
  9219. # $dbh->func(30000,'busy_timeout'); # milliseconds (SQLite)
  9220. section_time('sql-connect');
  9221. $self;
  9222. }
  9223. sub disconnect_from_sql($) {
  9224. my($self) = shift; $self->{in_transaction} = 0;
  9225. if ($self->dbh) {
  9226. do_log(4,"disconnecting from SQL");
  9227. $self->dbh->disconnect; $self->dbh(undef);
  9228. }
  9229. }
  9230. 1;
  9231. __DATA__
  9232. #^L
  9233. package Amavis::Out::SQL::Log;
  9234. use strict;
  9235. use re 'taint';
  9236. BEGIN {
  9237. use Exporter ();
  9238. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  9239. $VERSION = '2.043';
  9240. @ISA = qw(Exporter);
  9241. }
  9242. use DBI;
  9243. use Encode; # Perl 5.8 UTF-8 support
  9244. BEGIN {
  9245. import Amavis::Conf qw(:platform $myhostname c cr ca);
  9246. import Amavis::rfc2821_2822_Tools qw(qquote_rfc2821_local split_address
  9247. iso8601_utc_timestamp);
  9248. import Amavis::Util qw(ll do_log am_id untaint safe_decode add_entropy);
  9249. import Amavis::Out::SQL::Connection ();
  9250. }
  9251. sub new {
  9252. my($class,$conn_h) = @_; bless { conn_h=>$conn_h, incarnation=>0 }, $class;
  9253. }
  9254. sub DESTROY {
  9255. my($self) = shift;
  9256. eval { do_log(5,"Amavis::Out::SQL::Log DESTROY called") };
  9257. }
  9258. sub save_info_preliminary {
  9259. my($self, $conn,$msginfo) = @_;
  9260. my($addr) = $msginfo->sender; my($invdomain) = '';
  9261. if ($addr ne '') {
  9262. local($1);
  9263. my($localpart,$domain) = split_address($addr); $domain = lc($domain);
  9264. $localpart = lc($localpart) if !c('localpart_is_case_sensitive');
  9265. $domain = $1 if $domain=~/^\@?(.*?)\.*\z/s; # chop leading @ and trailing .
  9266. $addr = $localpart.'@'.$domain;
  9267. $addr = substr($addr,0,255) if length($addr) > 255;
  9268. $invdomain = join('.', reverse split(/\./,$domain,-1));
  9269. $invdomain = substr($invdomain,0,255) if length($invdomain) > 255;
  9270. }
  9271. my($conn_h) = $self->{conn_h}; my($sql_cl_r) = cr('sql_clause');
  9272. $conn_h->begin_work; # SQL transaction starts
  9273. eval {
  9274. # find an existing e-mail address record for sender, or insert a new one
  9275. my($sel_adr) = $sql_cl_r->{'sel_adr'};
  9276. my($ins_adr) = $sql_cl_r->{'ins_adr'};
  9277. my($sid,$a_ref);
  9278. $conn_h->execute($sel_adr,untaint($addr));
  9279. if ( defined($a_ref=$conn_h->fetchrow_arrayref($sel_adr)) ) {
  9280. $sid = $a_ref->[0]; $conn_h->finish($sel_adr);
  9281. } else { # does not exist, insert a new record for the e-mail address
  9282. $conn_h->execute($ins_adr,untaint($addr),untaint($invdomain));
  9283. $sid = $conn_h->last_insert_id(undef, undef, 'maddr', 'id');
  9284. $sid = $conn_h->sth($ins_adr)->{'mysql_insertid'} if !defined($sid);
  9285. if (defined $sid) { add_entropy($sid) }
  9286. else { $sid = 0; do_log(1,"sql: DBD does not support last_insert_id") }
  9287. }
  9288. do_log(4,"save_info_preliminary: $sid, $addr, ".($a_ref?'exists':'new'));
  9289. # insert a placeholder message record with sender information
  9290. $conn_h->execute($sql_cl_r->{'ins_msg'},
  9291. $msginfo->mail_id, $msginfo->secret_id, am_id(),
  9292. $msginfo->rx_time, iso8601_utc_timestamp($msginfo->rx_time),
  9293. untaint($sid), c('policy_bank_path'), untaint($msginfo->client_addr),
  9294. untaint($msginfo->msg_size), substr($myhostname,0,255));
  9295. $conn_h->commit;
  9296. };
  9297. if ($@ ne '') {
  9298. my($err) = $@; chomp($err);
  9299. eval { $conn_h->rollback };
  9300. do_log(1, "save_info_preliminary: rollback".($@ eq '' ? " done" : ": $@"));
  9301. do_log(-1, "WARN save_info_preliminary: $err");
  9302. return 0;
  9303. }
  9304. 1;
  9305. }
  9306. sub save_info_final {
  9307. my($self, $conn,$msginfo,$spam_level,$dsn_sent,$content_type) = @_;
  9308. my($lpcs) = c('localpart_is_case_sensitive');
  9309. my($mail_id) = $msginfo->mail_id;
  9310. my($conn_h) = $self->{conn_h}; my($sql_cl_r) = cr('sql_clause');
  9311. my($sel_adr,$ins_adr,$ins_rcp) = @$sql_cl_r{'sel_adr','ins_adr','ins_rcp'};
  9312. $conn_h->begin_work; # SQL transaction starts
  9313. eval {
  9314. for my $r (@{$msginfo->per_recip_data}) {
  9315. my($addr) = $r->recip_addr; my($invdomain) = '';
  9316. if ($addr ne '') {
  9317. local($1); my($localpart,$domain) = split_address($addr);
  9318. $domain = lc($domain); $localpart = lc($localpart) if !$lpcs;
  9319. $domain = $1 if $domain=~/^\@?(.*?)\.*\z/s; # chop leading @ and tr.dot
  9320. $addr = $localpart.'@'.$domain;
  9321. $addr = substr($addr,0,255) if length($addr) > 255;
  9322. $invdomain = join('.', reverse split(/\./,$domain,-1));
  9323. $invdomain = substr($invdomain,0,255) if length($invdomain) > 255;
  9324. }
  9325. # find an existing e-mail address record for recipients, or insert one
  9326. my($rid,$a_ref);
  9327. $conn_h->execute($sel_adr,untaint($addr));
  9328. if (defined($a_ref=$conn_h->fetchrow_arrayref($sel_adr))) {
  9329. $rid = $a_ref->[0]; $conn_h->finish($sel_adr);
  9330. } else { # does not exist, insert a new record with the e-mail address
  9331. $conn_h->execute($ins_adr,untaint($addr),untaint($invdomain));
  9332. $rid = $conn_h->last_insert_id(undef, undef, 'maddr', 'id');
  9333. $rid = $conn_h->sth($ins_adr)->{'mysql_insertid'} if !defined($rid);
  9334. if (defined $rid) { add_entropy($rid) }
  9335. else { $rid = 0; do_log(1,"sql: DBD does not support last_insert_id") }
  9336. }
  9337. do_log(4,"save_info_final $mail_id, recip id: $rid, $addr, ".
  9338. ($a_ref?'exists':'new'));
  9339. my($dest,$resp) = ($r->recip_destiny, $r->recip_smtp_response);
  9340. my($d) = $resp=~/^4/ ? 'TEMPFAIL'
  9341. : ($dest==D_BOUNCE && $resp=~/^5/) ? 'BOUNCE'
  9342. : ($dest!=D_BOUNCE && $resp=~/^5/) ? 'REJECT'
  9343. : ($dest==D_PASS && ($resp=~/^2/ || !$r->recip_done)) ? 'PASS'
  9344. : ($dest==D_DISCARD) ? 'DISCARD' : '?';
  9345. # insert recipient record
  9346. $conn_h->execute($ins_rcp,
  9347. $mail_id, untaint($rid), substr($d,0,1), ' ',
  9348. $r->recip_blacklisted_sender ? 'Y' : 'N',
  9349. $r->recip_whitelisted_sender ? 'Y' : 'N',
  9350. !defined($spam_level) ? undef :
  9351. untaint($spam_level)+$r->recip_score_boost,
  9352. untaint($resp) );
  9353. };
  9354. my($m_id) = ''; my($from) = ''; my($subj) = '';
  9355. my($ent) = $msginfo->mime_entity;
  9356. if (!defined $ent) {
  9357. do_log(4,"save_info_final: no MIME entity, header info not available");
  9358. } else { # if message header has been parsed by MIME-Tools
  9359. $m_id = $ent->head->get('Message-ID',0);
  9360. $from = $ent->head->get('From',0);
  9361. $subj = $ent->head->get('Subject',0);
  9362. for ($m_id,$from,$subj) {
  9363. local($1); chomp;
  9364. s/\n([ \t])/$1/sg; s/^[ \t]+//s; s/[ \t]+\z//s; # unfold, trim
  9365. if ($unicode_aware) {
  9366. my($octets); # string of bytes (not logical chars), UTF8 encoded
  9367. eval { $octets = Encode::encode_utf8(safe_decode('MIME-Header',$_))};
  9368. if ($@ eq '') { $_ = $octets }
  9369. else { do_log(1,"save_info_final INFO: header field ".
  9370. "not decodable, keeping raw bytes: $@") }
  9371. }
  9372. $_ = substr($_,0,255) if length($_) > 255;
  9373. }
  9374. }
  9375. my($quar_type) = $msginfo->quar_type;
  9376. for ($quar_type,$content_type) { $_ = ' ' if !defined || /^ *\z/ }
  9377. do_log(4,"save_info_final $mail_id, $quar_type, $content_type, $dsn_sent,".
  9378. " $spam_level, Message-ID: $m_id, From: '$from', Subject: '$subj'");
  9379. # update message record with additional information
  9380. $conn_h->execute($sql_cl_r->{'upd_msg'},
  9381. $content_type, $quar_type, $dsn_sent, untaint($spam_level),
  9382. untaint($m_id), untaint($from), untaint($subj), $mail_id);
  9383. $conn_h->commit;
  9384. };
  9385. if ($@ ne '') {
  9386. my($err) = $@; chomp($err);
  9387. eval { $conn_h->rollback };
  9388. do_log(1, "save_info_final: rollback".($@ eq '' ? " done" : ": $@"));
  9389. do_log(-1, "WARN save_info_final: $err");
  9390. return 0;
  9391. }
  9392. 1;
  9393. }
  9394. 1;
  9395. __DATA__
  9396. #
  9397. package Amavis::IO::SQL;
  9398. # a simple IO wrapper around SQL for inserting/retrieving mail text
  9399. # to/from a database
  9400. use strict;
  9401. use re 'taint';
  9402. BEGIN {
  9403. use Exporter ();
  9404. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  9405. $VERSION = '2.043';
  9406. @ISA = qw(Exporter);
  9407. }
  9408. use Errno qw(ENOENT EACCES EIO);
  9409. use DBI;
  9410. BEGIN {
  9411. import Amavis::Util qw(ll do_log untaint);
  9412. }
  9413. sub new {
  9414. my($class) = shift; my($self) = bless {}, $class;
  9415. if (@_) { $self->open(@_) or return undef }
  9416. $self;
  9417. }
  9418. sub open {
  9419. my($self) = shift; @$self{qw(conn_h clause dbkey mode maxbuf)} = @_;
  9420. $self->{buf} = '';
  9421. $self->{chunk_ind} = $self->{pos} = $self->{bufpos} = $self->{eof} = 0;
  9422. if ($self->{mode} ne 'w') {
  9423. eval { $self->{conn_h}->execute($self->{clause}, $self->{dbkey}) };
  9424. my($ll) = $@ ne '' ? -1 : 4;
  9425. ll($ll) && do_log($ll,sprintf("Amavis::IO::SQL::open (%s); key=%s: %s",
  9426. $self->{clause}, $self->{dbkey}, $@));
  9427. if ($@ ne '') {
  9428. chomp($@); die "Amavis::IO::SQL::open error: $@";
  9429. $! = EIO; return undef; # not reached
  9430. }
  9431. }
  9432. $self;
  9433. }
  9434. sub DESTROY {
  9435. my($self) = shift;
  9436. if (ref $self && $self->{conn_h}) {
  9437. eval { $self->close or die "Error closing: $!" };
  9438. if ($@ ne '') { warn "Amavis::IO::SQL::close error: $@" }
  9439. delete $self->{conn_h};
  9440. }
  9441. }
  9442. sub close {
  9443. my($self) = shift; $@ = undef;
  9444. eval {
  9445. if ($self->{mode} eq 'w') {
  9446. $self->flush or die "Can't flush: $!";
  9447. } elsif ($self->{conn_h} && $self->{clause} && !$self->{eof}) {
  9448. # reading, closing before eof was reached
  9449. $self->{conn_h}->finish($self->{clause}) or die "Can't finish: $!";
  9450. }
  9451. };
  9452. delete @$self{
  9453. qw(conn_h clause dbkey mode maxbuf buf chunk_ind pos bufpos eof) };
  9454. if ($@ ne '') {
  9455. chomp($@); die "Error closing, $@";
  9456. $! = EIO; return undef; # not reached
  9457. }
  9458. 1;
  9459. }
  9460. sub seek {
  9461. my($self,$pos,$whence) = @_;
  9462. $whence==0 && $pos==0 or die "Seek to $whence,$pos on sql i/o not supported";
  9463. ll(5) && do_log(5, "Amavis::IO::SQL::seek mode=".$self->{mode});
  9464. $self->{mode} ne 'w'
  9465. or die "Seek to $whence,$pos on sql i/o only supported for read mode";
  9466. if ($self->{chunk_ind} <= 1) # still in the first chunk, just reset bufpos
  9467. { $self->{pos} = $self->{bufpos} = $self->{eof} = 0; 1 } # reset, success
  9468. else { # beyond the first chunk, need to restart the query from the beginning
  9469. my($con,$clause,$key,$mode,$maxb) =
  9470. @$self{qw(conn_h clause dbkey mode maxbuf)};
  9471. $self->close or die "seek: error closing, $!";
  9472. $self->open($con,$clause,$key,$mode,$maxb)
  9473. or die "seek: reopen failed, $!";
  9474. }
  9475. 1;
  9476. }
  9477. sub read { # SCALAR,LENGTH,OFFSET
  9478. my($self) = shift;
  9479. !defined($_[2]) || $_[2]==0
  9480. or die "Reading from sql to an offset not supported";
  9481. my($req_len) = $_[1]; my($conn_h) = $self->{conn_h}; my($a_ref);
  9482. ll(5) && do_log(5, "Amavis::IO::SQL::read, ".
  9483. $self->{chunk_ind}.", ".$self->{bufpos});
  9484. eval {
  9485. while (!$self->{eof} && length($self->{buf})-$self->{bufpos} < $req_len) {
  9486. $a_ref = $conn_h->fetchrow_arrayref($self->{clause});
  9487. if (!defined($a_ref)) { $self->{eof} = 1 }
  9488. else { $self->{buf} .= $a_ref->[0]; $self->{chunk_ind}++ }
  9489. }
  9490. };
  9491. if ($@ ne '') {
  9492. # we can't stash an arbitrary error message string into $!,
  9493. # which forces us to use 'die' to properly report an error
  9494. chomp($@); die "read: sql select failed, $@";
  9495. $! = EIO; return undef; # not reached
  9496. };
  9497. $_[0] = substr($self->{buf}, $self->{bufpos}, $req_len);
  9498. my($nbytes) = length($_[0]);
  9499. $self->{bufpos} += $nbytes; $self->{pos} += $nbytes;
  9500. if ($self->{bufpos} > 0 && $self->{chunk_ind} > 1) {
  9501. # discard used-up part of the buf unless at ch.1, which may still be useful
  9502. do_log(5,"read: moving on by ".$self->{bufpos}." chars");
  9503. $self->{buf} = substr($self->{buf},$self->{bufpos}); $self->{bufpos} = 0;
  9504. }
  9505. $nbytes; # eof: 0, error: undef
  9506. }
  9507. sub getline {
  9508. my($self) = shift; my($conn_h) = $self->{conn_h};
  9509. ll(5) && do_log(5, "Amavis::IO::SQL::getline, ".
  9510. $self->{chunk_ind}.", ".$self->{bufpos});
  9511. my($a_ref,$line); my($ind) = -1;
  9512. eval {
  9513. while (!$self->{eof} &&
  9514. ($ind=index($self->{buf},"\n",$self->{bufpos})) < 0) {
  9515. $a_ref = $conn_h->fetchrow_arrayref($self->{clause});
  9516. if (!defined($a_ref)) { $self->{eof} = 1 }
  9517. else { $self->{buf} .= $a_ref->[0]; $self->{chunk_ind}++ }
  9518. }
  9519. };
  9520. if ($@ ne '') {
  9521. chomp($@); die "getline: reading sql select results failed, $@";
  9522. $! = EIO; return undef; # not reached
  9523. };
  9524. if ($ind < 0 && $self->{eof}) # imply a NL before eof if missing
  9525. { $self->{buf} .= "\n"; $ind = index($self->{buf}, "\n", $self->{bufpos}) }
  9526. $ind >= 0 or die "Programming error, NL not found";
  9527. if (length($self->{buf}) > $self->{bufpos}) { # nonempty buffer?
  9528. $line = substr($self->{buf}, $self->{bufpos}, $ind+1-$self->{bufpos});
  9529. my($nbytes) = length($line);
  9530. $self->{bufpos} += $nbytes; $self->{pos} += $nbytes;
  9531. if ($self->{bufpos} > 0 && $self->{chunk_ind} > 1) {
  9532. # discard used-up part of the buf unless at ch.1, which may still be useful
  9533. ll(5) && do_log(5,"getline: moving on by ".$self->{bufpos}." chars");
  9534. $self->{buf} = substr($self->{buf},$self->{bufpos}); $self->{bufpos} = 0;
  9535. }
  9536. }
  9537. # eof: undef, $! zero; error: undef, $! nonzero
  9538. $! = 0; $line eq '' ? undef : $line;
  9539. }
  9540. sub flush {
  9541. my($self) = shift;
  9542. $self->{mode} eq 'w' or die "Can't flush, opened for reading";
  9543. my($msg); my($conn_h) = $self->{conn_h};
  9544. while (length($self->{buf}) > 0) {
  9545. my($ind) = $self->{chunk_ind} + 1;
  9546. ll(4) && do_log(4, sprintf("sql flush: key: (%s, %d), size=%d",
  9547. $self->{dbkey}, $ind,
  9548. length($self->{buf}) < $self->{maxbuf} ? length($self->{buf})
  9549. : $self->{maxbuf} ));
  9550. eval {
  9551. $conn_h->execute($self->{clause}, $self->{dbkey}, $ind,
  9552. untaint(substr($self->{buf},0,$self->{maxbuf})));
  9553. };
  9554. if ($@ ne '') { $msg = $@; last }
  9555. substr($self->{buf},0,$self->{maxbuf}) = ''; $self->{chunk_ind} = $ind;
  9556. }
  9557. if (defined($msg)) {
  9558. chomp($msg); $msg = "flush: sql inserting text failed, $msg";
  9559. die $msg; # we can't stash an arbitrary error message string into $!,
  9560. # which forces us to use 'die' to properly report an error
  9561. $! = EIO; return undef; # not reached
  9562. }
  9563. 1;
  9564. }
  9565. sub print {
  9566. my($self) = shift;
  9567. $self->{mode} eq 'w' or die "Can't print, not opened for writing";
  9568. my($nbytes); my($conn_h) = $self->{conn_h}; my($len) = length($_[0]);
  9569. if ($len <= 0) { $nbytes = "0 but true" }
  9570. else {
  9571. $self->{buf} .= $_[0]; $self->{pos} += $len; $nbytes = $len;
  9572. while (length($self->{buf}) >= $self->{maxbuf}) {
  9573. my($ind) = $self->{chunk_ind} + 1;
  9574. ll(4) && do_log(4, sprintf("sql print: key: (%s, %d), size=%d",
  9575. $self->{dbkey}, $ind, $self->{maxbuf}));
  9576. eval {
  9577. $conn_h->execute($self->{clause}, $self->{dbkey}, $ind,
  9578. untaint(substr($self->{buf},0,$self->{maxbuf})));
  9579. };
  9580. if ($@ ne '') {
  9581. # we can't stash an arbitrary error message string into $!,
  9582. # which forces us to use 'die' to properly report an error
  9583. chomp($@); die "print: sql inserting mail text failed, $@";
  9584. $! = EIO; return undef; # not reached
  9585. };
  9586. substr($self->{buf},0,$self->{maxbuf}) = ''; $self->{chunk_ind} = $ind;
  9587. }
  9588. }
  9589. $nbytes;
  9590. }
  9591. sub printf { shift->print(sprintf(shift,@_)) }
  9592. 1;
  9593. #^L
  9594. package Amavis::Out::SQL::Quarantine;
  9595. use strict;
  9596. use re 'taint';
  9597. BEGIN {
  9598. use Exporter ();
  9599. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  9600. $VERSION = '2.043';
  9601. @ISA = qw(Exporter);
  9602. @EXPORT = qw(&mail_via_sql);
  9603. }
  9604. use subs @EXPORT;
  9605. use DBI;
  9606. use IO::Wrap;
  9607. BEGIN {
  9608. import Amavis::Conf qw(:platform c cr ca);
  9609. import Amavis::rfc2821_2822_Tools qw(qquote_rfc2821_local);
  9610. import Amavis::Util qw(ll do_log am_id snmp_count);
  9611. import Amavis::Timing qw(section_time);
  9612. import Amavis::Out::SQL::Connection ();
  9613. }
  9614. sub mail_via_sql {
  9615. my($conn_h,$msginfo,$initial_submission,$dsn_per_recip_capable,$filter) = @_;
  9616. snmp_count('OutMsgs'); local($1);
  9617. my($mail_id) = $msginfo->mail_id;
  9618. my(@per_recip_data) = grep { !$_->recip_done && (!$filter || &$filter($_)) }
  9619. @{$msginfo->per_recip_data};
  9620. my($logmsg) = sprintf("%s via SQL: %s", ($initial_submission?'SEND':'FWD'),
  9621. qquote_rfc2821_local($msginfo->sender));
  9622. if (!@per_recip_data) { do_log(5, "$logmsg, nothing to do"); return 1 }
  9623. do_log(1, $logmsg . " -> " .
  9624. qquote_rfc2821_local(map {$_->recip_final_addr} @per_recip_data) .
  9625. ", mail_id $mail_id");
  9626. my($msg) = $msginfo->mail_text; # a scalar reference, or a file handle
  9627. if (defined($msg) && !$msg->isa('MIME::Entity')) {
  9628. $msg = IO::Wrap::wraphandle($msg); # now we have an IO::Handle-like obj
  9629. $msg->seek(0,0) or die "Can't rewind mail file: $!";
  9630. }
  9631. eval {
  9632. my($sql_cl_r) = cr('sql_clause');
  9633. $conn_h->begin_work; # SQL transaction starts
  9634. eval {
  9635. my($mp) = Amavis::IO::SQL->new;
  9636. $mp->open($conn_h, $sql_cl_r->{'ins_quar'},$msginfo->mail_id,'w',16384)
  9637. or die "Can't open Amavis::IO::SQL object: $!";
  9638. my($hdr_edits) = $msginfo->header_edits;
  9639. $hdr_edits = Amavis::Out::EditHeader->new if !$hdr_edits;
  9640. my($received_cnt) = $hdr_edits->write_header($msg,$mp);
  9641. if ($received_cnt > 100) { # loop detection required by rfc2821 6.2
  9642. die "Too many hops: $received_cnt 'Received:' header lines";
  9643. } elsif (!defined($msg)) { # empty mail body
  9644. } elsif ($msg->isa('MIME::Entity')) {
  9645. $msg->print_body($mp);
  9646. } else {
  9647. my($nbytes,$buff);
  9648. while (($nbytes=$msg->read($buff,16384)) > 0)
  9649. { $mp->print($buff) or die "Can't write to SQL sorage: $!" }
  9650. defined $nbytes or die "Error reading: $!";
  9651. }
  9652. $mp->close or die "Error closing Amavis::IO::SQL object: $!";
  9653. $conn_h->commit;
  9654. };
  9655. if ($@ ne '') {
  9656. my($msg) = $@; chomp($msg);
  9657. $msg = "writing mail text to SQL failed: $msg"; do_log(0,$msg);
  9658. eval { $conn_h->rollback };
  9659. do_log(1, "mail_via_sql: rollback".($@ eq '' ? " done" : ": $@"));
  9660. die $msg;
  9661. }
  9662. };
  9663. my($err) = $@; my($smtp_response);
  9664. if ($err eq '') {
  9665. $smtp_response = "250 2.6.0 Ok, Stored to sql db as mail_id $mail_id";
  9666. snmp_count('OutMsgsDelivers');
  9667. } else {
  9668. chomp($err);
  9669. if ($err =~ /too many hops/i) {
  9670. $smtp_response = "550 5.4.6 Rejected: $err";
  9671. snmp_count('OutMsgsRejects');
  9672. } else {
  9673. $smtp_response = "451 4.5.0 Storing to sql db as mail_id $mail_id failed: $err";
  9674. snmp_count('OutAttemptFails');
  9675. }
  9676. }
  9677. $smtp_response .= ", id=" . am_id();
  9678. for my $r (@per_recip_data) {
  9679. next if $r->recip_done;
  9680. $r->recip_smtp_response($smtp_response); $r->recip_done(2);
  9681. $r->recip_mbxname($mail_id) if $smtp_response =~ /^2/;
  9682. }
  9683. section_time('fwd-sql');
  9684. 1;
  9685. }
  9686. __DATA__
  9687. #
  9688. package Amavis::Lookup::SQLfield;
  9689. use strict;
  9690. use re 'taint';
  9691. BEGIN {
  9692. use Exporter ();
  9693. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  9694. $VERSION = '2.043';
  9695. @ISA = qw(Exporter);
  9696. }
  9697. BEGIN { import Amavis::Util qw(ll do_log) }
  9698. sub new($$$;$$) {
  9699. my($class, $sql_query,$fieldname, $fieldtype,$implied_args) = @_;
  9700. # fieldtype: B=boolean, N=numeric, S=string,
  9701. # N-: numeric, nonexistent field returns undef without complaint
  9702. # S-: string, nonexistent field returns undef without complaint
  9703. # B-: boolean, nonexistent field returns undef without complaint
  9704. # B0: boolean, nonexistent field treated as false
  9705. # B1: boolean, nonexistent field treated as true
  9706. return undef if !defined($sql_query);
  9707. my($self) = bless {}, $class;
  9708. $self->{sql_query} = $sql_query;
  9709. $self->{fieldname} = lc($fieldname);
  9710. $self->{fieldtype} = uc($fieldtype);
  9711. $self->{args} = ref($implied_args) eq 'ARRAY' ? [@$implied_args] # copy
  9712. : [$implied_args] if defined $implied_args;
  9713. $self;
  9714. }
  9715. sub lookup_sql_field($$$) {
  9716. my($self,$addr,$get_all) = @_;
  9717. my(@result,@matchingkey);
  9718. if (!defined($self)) {
  9719. do_log(5, "lookup_sql_field - undefined, \"$addr\" no match");
  9720. } elsif (!defined($self->{sql_query})) {
  9721. do_log(5, sprintf("lookup_sql_field(%s) - null query, \"%s\" no match",
  9722. $self->{fieldname}, $addr));
  9723. } else {
  9724. my($field) = $self->{fieldname};
  9725. my($res_ref,$mk_ref) = $self->{sql_query}->lookup_sql($addr,1,
  9726. !exists($self->{args}) ? () : $self->{args});
  9727. do_log(5, "lookup_sql_field($field), \"$addr\" no matching records")
  9728. if !defined($res_ref) || !@$res_ref;
  9729. for my $ind (0 .. (!defined($res_ref) ? -1 : $#$res_ref)) {
  9730. my($match); my($h_ref) = $res_ref->[$ind]; my($mk) = $mk_ref->[$ind];
  9731. if (!exists($h_ref->{$field})) {
  9732. # record found, but no field with that name in the table
  9733. # fieldtype: B0: boolean, nonexistent field treated as false,
  9734. # B1: boolean, nonexistent field treated as true
  9735. if ( $self->{fieldtype} =~ /^B0/) { # boolean, defaults to false
  9736. $match = 0; # nonexistent field treated as 0
  9737. do_log(5, "lookup_sql_field($field), no field, \"$addr\" result=$match");
  9738. } elsif ($self->{fieldtype} =~ /^B1/) { # defaults to true
  9739. $match = 1; # nonexistent field treated as 1
  9740. do_log(5,"lookup_sql_field($field), no field, \"$addr\" result=$match");
  9741. } elsif ($self->{fieldtype}=~/^.-/s) { # allowed to not exist
  9742. do_log(5,"lookup_sql_field($field), no field, \"$addr\" result=undef");
  9743. } else { # treated as 'no match', issue a warning
  9744. do_log(1,"lookup_sql_field($field) ".
  9745. "(WARN: no such field in the SQL table), ".
  9746. "\"$addr\" result=undef");
  9747. }
  9748. } else { # field exists
  9749. # fieldtype: B=boolean, N=numeric, S=string
  9750. $match = $h_ref->{$field};
  9751. if (!defined($match)) { # NULL field values represented as undef
  9752. } elsif ($self->{fieldtype} =~ /^B/) { # boolean
  9753. # convert values 'N', 'F', '0', ' ' and "\000" to 0
  9754. # to allow value to be used directly as a Perl boolean
  9755. $match = 0 if $match =~ /^([NnFf ]|0+|\000+)[ ]*\z/;
  9756. } elsif ($self->{fieldtype} =~ /^N/) { # numeric
  9757. $match = $match + 0; # unify different numeric forms
  9758. } elsif ($self->{fieldtype} =~ /^S/) { # string
  9759. $match =~ s/ +\z//; # trim trailing spaces
  9760. }
  9761. do_log(5, "lookup_sql_field($field) \"$addr\" result=" .
  9762. (defined $match ? $match : 'undef') );
  9763. }
  9764. if (defined $match) {
  9765. push(@result,$match); push(@matchingkey,$mk);
  9766. last if !$get_all;
  9767. }
  9768. }
  9769. }
  9770. if (!$get_all) { !wantarray ? $result[0] : ($result[0], $matchingkey[0]) }
  9771. else { !wantarray ? \@result : (\@result, \@matchingkey) }
  9772. }
  9773. 1;
  9774. #
  9775. package Amavis::Lookup::SQL;
  9776. use strict;
  9777. use re 'taint';
  9778. BEGIN {
  9779. use Exporter ();
  9780. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  9781. $VERSION = '2.043';
  9782. @ISA = qw(Exporter);
  9783. }
  9784. use DBI;
  9785. BEGIN {
  9786. import Amavis::Conf qw(:platform :confvars c cr ca);
  9787. import Amavis::Timing qw(section_time);
  9788. import Amavis::Util qw(untaint snmp_count ll do_log);
  9789. import Amavis::rfc2821_2822_Tools qw(make_query_keys);
  9790. import Amavis::Out::SQL::Connection ();
  9791. }
  9792. # return a new Lookup::SQL object to contain DBI handle and prepared selects
  9793. sub new {
  9794. my($class, $conn_h, $clause_name) = @_;
  9795. if ($clause_name eq '') { undef }
  9796. else {
  9797. # $clause_name is an key into %sql_clause of the currently selected
  9798. # policy bank; one level of indirection is allowed in %sql_clause result,
  9799. # the resulting SQL clause may include %k, to be expanded
  9800. bless { conn_h => $conn_h, incarnation => 0, clause_name => $clause_name },
  9801. $class;
  9802. }
  9803. }
  9804. sub DESTROY {
  9805. my($self) = shift; eval { do_log(5,"Amavis::Lookup::SQL DESTROY called") };
  9806. }
  9807. sub init {
  9808. my($self) = @_;
  9809. if ($self->{incarnation} != $self->{conn_h}->incarnation) { # invalidated?
  9810. $self->{incarnation} = $self->{conn_h}->incarnation;
  9811. $self->clear_cache; # db handle has changed, invalidate cache
  9812. }
  9813. $self;
  9814. }
  9815. sub clear_cache {
  9816. my($self) = @_;
  9817. delete $self->{cache};
  9818. }
  9819. # lookup_sql() performs a lookup for an e-mail address against a SQL map.
  9820. # If a match is found it returns whatever the map returns (a reference
  9821. # to a hash containing values of requested fields), otherwise returns undef.
  9822. # A match aborts further fetching sequence, unless $get_all is true.
  9823. #
  9824. # SQL lookups (e.g. for user+foo@example.com) are performed in order
  9825. # which can be requested by 'ORDER BY' in the SELECT statement, otherwise
  9826. # the order is unspecified, which is only useful if only specific entries
  9827. # exist in a database (e.g. only full addresses, not domains).
  9828. #
  9829. # The following order is recommended, going from specific to more general:
  9830. # - lookup for user+foo@example.com
  9831. # - lookup for user@example.com (only if $recipient_delimiter nonempty)
  9832. # - lookup for user+foo ('naked lookup': only if local)
  9833. # - lookup for user ('naked lookup': local and $recipient_delimiter nonempty)
  9834. # - lookup for @sub.example.com
  9835. # - lookup for @.sub.example.com
  9836. # - lookup for @.example.com
  9837. # - lookup for @.com
  9838. # - lookup for @. (catchall)
  9839. # NOTE:
  9840. # this is different from hash and ACL lookups in two important aspects:
  9841. # - a key without '@' implies mailbox (=user) name, not domain name;
  9842. # - the naked mailbox name lookups are only performed when the e-mail addr
  9843. # (usually its domain part) matches the static local_domains* lookups.
  9844. #
  9845. # The domain part is always lowercased when constructing a key,
  9846. # the localpart is lowercased unless $localpart_is_case_sensitive is true.
  9847. #
  9848. sub lookup_sql($$$;$) {
  9849. my($self, $addr,$get_all,$extra_args) = @_;
  9850. my(@matchingkey,@result);
  9851. my($sel); my($sql_cl_r) = cr('sql_clause');
  9852. $sel = $sql_cl_r->{$self->{clause_name}} if defined $sql_cl_r;
  9853. $sel = $$sel if ref $sel eq 'SCALAR'; # allow one level of indirection
  9854. if (!defined($sel) || $sel eq '') {
  9855. ll(4) && do_log(4,"lookup_sql disabled for clause: ".$self->{clause_name});
  9856. return(!wantarray ? undef : (undef,undef));
  9857. } elsif (!defined $extra_args &&
  9858. exists $self->{cache} && exists $self->{cache}->{$addr})
  9859. { # cached ?
  9860. my($c) = $self->{cache}->{$addr}; @result = @$c if ref $c;
  9861. @matchingkey = map {'/cached/'} @result; #will do for now, improve some day
  9862. # if (!ll(5)) {}# don't bother preparing log report which will not be printed
  9863. # elsif (!@result) { do_log(5,"lookup_sql (cached): \"$addr\" no match") }
  9864. # else {
  9865. # for my $m (@result) {
  9866. # do_log(5, sprintf("lookup_sql (cached): \"%s\" matches, result=(%s)",
  9867. # $addr, join(", ", map { sprintf("%s=>%s", $_,
  9868. # !defined($m->{$_})?'-':'"'.$m->{$_}.'"'
  9869. # ) } sort keys(%$m) ) ));
  9870. # }
  9871. # }
  9872. if (!$get_all) {
  9873. return(!wantarray ? $result[0] : ($result[0], $matchingkey[0]));
  9874. } else {
  9875. return(!wantarray ? \@result : (\@result, \@matchingkey));
  9876. }
  9877. }
  9878. my($is_local); # $local_domains_sql is not looked up to avoid recursion!
  9879. $is_local = Amavis::Lookup::lookup(0,$addr,
  9880. grep {ref ne 'Amavis::Lookup::SQL' &&
  9881. ref ne 'Amavis::Lookup::SQLfield' &&
  9882. ref ne 'Amavis::Lookup::LDAP' &&
  9883. ref ne 'Amavis::Lookup::LDAPattr'}
  9884. @{ca('local_domains_maps')});
  9885. my($keys_ref,$rhs_ref) = make_query_keys($addr,0,$is_local);
  9886. my($n) = sprintf("%d",scalar(@$keys_ref)); # number of keys
  9887. my(@pos_args); my(@extras_tmp) = !ref $extra_args ? () : @$extra_args;
  9888. $sel =~ s{ ( %k | \? ) } # substitute %k for keys and ? for each extra arg
  9889. { push(@pos_args, map { untaint($_) }
  9890. $1 eq '%k' ? @$keys_ref : shift @extras_tmp),
  9891. $1 eq '%k' ? join(',', ('?') x $n) : '?' }gxe;
  9892. ll(4) && do_log(4,"lookup_sql \"$addr\", query args: ".
  9893. join(', ', map{"\"$_\""} @pos_args));
  9894. ll(4) && do_log(4,"lookup_sql select: $sel");
  9895. my($a_ref,$found); my($match) = {}; my($conn_h) = $self->{conn_h};
  9896. $conn_h->begin_work_nontransaction; # (re)connect if not connected
  9897. eval {
  9898. snmp_count('OpsSqlSelect');
  9899. $conn_h->execute($sel,@pos_args); # do the query
  9900. # fetch query results
  9901. while ( defined($a_ref=$conn_h->fetchrow_arrayref($sel)) ) {
  9902. my(@names) = @{$conn_h->sth($sel)->{NAME_lc}};
  9903. $match = {}; @$match{@names} = @$a_ref;
  9904. if (!exists $match->{'local'} && $match->{'email'} eq '@.') {
  9905. # UGLY HACK to let a catchall (@.) imply that field 'local' has
  9906. # a value undef (NULL) when that field is not present in the
  9907. # database. This overrides B1 fieldtype default by an explicit
  9908. # undef for '@.', causing a fallback to static lookup tables.
  9909. # The purpose is to provide a useful default for local_domains
  9910. # lookup if the field 'local' is not present in the SQL table.
  9911. # NOTE: field names 'local' and 'email' are hardwired here!!!
  9912. push(@names,'local'); $match->{'local'} = undef;
  9913. do_log(5, "lookup_sql: \"$addr\" matches catchall, local=>undef");
  9914. }
  9915. push(@result, {%$match}); # copy hash
  9916. push(@matchingkey, join(", ", map { sprintf("%s=>%s", $_,
  9917. !defined($match->{$_})?'-':'"'.$match->{$_}.'"'
  9918. ) } @names));
  9919. last if !$get_all;
  9920. }
  9921. $conn_h->finish($sel) if defined $a_ref; # only if not all read
  9922. }; # eval
  9923. if ($@ ne '') {
  9924. my($err) = $@; chomp($err);
  9925. do_log(-1, "lookup_sql: $err, $DBI::err, $DBI::errstr");
  9926. die $err;
  9927. }
  9928. if (!ll(4)) {
  9929. # don't bother preparing log report which will not be printed
  9930. } elsif (!@result) {
  9931. do_log(4, "lookup_sql, \"$addr\" no match")
  9932. } else {
  9933. do_log(4, "lookup_sql($addr) matches, result=($_)") for @matchingkey;
  9934. }
  9935. # save for future use, but only within processing of this message
  9936. $self->{cache}->{$addr} = \@result;
  9937. section_time('lookup_sql');
  9938. if (!$get_all) { !wantarray ? $result[0] : ($result[0], $matchingkey[0]) }
  9939. else { !wantarray ? \@result : (\@result, \@matchingkey) }
  9940. }
  9941. 1;
  9942. __DATA__
  9943. #^L
  9944. package Amavis::LDAP::Connection;
  9945. use strict;
  9946. use re 'taint';
  9947. BEGIN {
  9948. use Exporter ();
  9949. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION
  9950. $ldap_sys_default);
  9951. $VERSION= '2.043';
  9952. @ISA = qw(Exporter);
  9953. import Amavis::Conf qw(:platform :confvars c cr ca);
  9954. import Amavis::Util qw(ll do_log);
  9955. import Amavis::Timing qw(section_time);
  9956. $ldap_sys_default = {
  9957. hostname => 'localhost',
  9958. port => 389,
  9959. version => 3,
  9960. timeout => 120,
  9961. tls => 0,
  9962. bind_dn => undef,
  9963. bind_password => undef,
  9964. };
  9965. }
  9966. sub new {
  9967. my($class,$default) = @_;
  9968. my($self) = bless {}, $class;
  9969. $self->{ldap} = undef;
  9970. $self->{incarnation} = 1;
  9971. $ldap_sys_default->{port} = 636 if $default->{hostname} =~ /^ldaps/;
  9972. for (qw(hostname port timeout tls base scope bind_dn bind_password)) {
  9973. # replace undefined attributes with user values or defaults
  9974. $self->{$_} = $default->{$_} unless defined($self->{$_});
  9975. $self->{$_} = $ldap_sys_default->{$_} unless defined($self->{$_});
  9976. }
  9977. $self;
  9978. }
  9979. sub ldap { # get/set ldap handle
  9980. my($self)=shift;
  9981. !@_ ? $self->{ldap} : ($self->{ldap}=shift);
  9982. }
  9983. sub DESTROY {
  9984. my($self)=shift;
  9985. eval { do_log(5,"Amavis::LDAP::Connection DESTROY called") };
  9986. eval { $self->disconnect_from_ldap };
  9987. }
  9988. sub incarnation { my($self)=shift; $self->{incarnation} }
  9989. sub begin_work {
  9990. my($self)=shift;
  9991. do_log(5,"ldap begin_work");
  9992. $self->ldap or $self->connect_to_ldap;
  9993. }
  9994. sub connect_to_ldap {
  9995. my($self) = shift;
  9996. my($bind_err,$start_tls_err);
  9997. do_log(3,"Connecting to LDAP server");
  9998. my $hostlist = ref $self->{hostname} eq 'ARRAY' ?
  9999. join(", ",@{$self->{hostname}}) : $self->{hostname};
  10000. do_log(4,"connect_to_ldap: trying $hostlist");
  10001. my $ldap = Net::LDAP->new($self->{hostname},
  10002. port => $self->{port},
  10003. version => $self->{version},
  10004. timeout => $self->{timeout},
  10005. );
  10006. if ($ldap) {
  10007. do_log(3,"connect_to_ldap: connected to $hostlist");
  10008. if ($self->{tls}) { # TLS required
  10009. my($mesg) = $ldap->start_tls(verify=>'none');
  10010. if ($mesg->code) { # start TLS failed
  10011. my($err) = $mesg->error_name;
  10012. do_log(-1,"connect_to_ldap: start TLS failed: $err");
  10013. $self->ldap(undef);
  10014. $start_tls_err = 1;
  10015. } else { # started TLS
  10016. do_log(3,"connect_to_ldap: TLS version $mesg enabled");
  10017. }
  10018. }
  10019. if ($self->{bind_dn}) { # bind required
  10020. my($mesg) = $ldap->bind($self->{bind_dn},
  10021. password => $self->{bind_password});
  10022. if ($mesg->code) { # bind failed
  10023. my($err) = $mesg->error_name;
  10024. do_log(-1,"connect_to_ldap: bind failed: $err");
  10025. $self->ldap(undef);
  10026. $bind_err = 1;
  10027. } else { # bind succeeded
  10028. do_log(3,"connect_to_ldap: bind $self->{bind_dn} succeeded");
  10029. }
  10030. }
  10031. } else { # connect failed
  10032. do_log(-1,"connect_to_ldap: unable to connect to host $hostlist");
  10033. }
  10034. $self->ldap($ldap); $self->{incarnation}++;
  10035. $ldap or die "connect_to_ldap: unable to connect";
  10036. if ($start_tls_err) { die "connect_to_ldap: start TLS failed" }
  10037. if ($bind_err) { die "connect_to_ldap: bind failed" }
  10038. section_time('ldap-connect');
  10039. $self;
  10040. }
  10041. sub disconnect_from_ldap {
  10042. my($self)=shift;
  10043. if ($self->ldap) {
  10044. do_log(4,"disconnecting from LDAP");
  10045. $self->ldap->disconnect;
  10046. $self->ldap(undef);
  10047. }
  10048. }
  10049. sub do_search {
  10050. my($self,$base,$scope,$filter) = @_;
  10051. my($result);
  10052. $self->ldap or die "do_search: ldap not available";
  10053. do_log(5,sprintf(
  10054. "lookup_ldap: searching base=\"%s\", scope=\"%s\", filter=\"%s\"",
  10055. $base, $scope, $filter));
  10056. eval {
  10057. $result = $self->{ldap}->search(base => $base,
  10058. scope => $scope,
  10059. filter => $filter,
  10060. );
  10061. if ($result->code) { die $result->error_name, "\n"; }
  10062. };
  10063. if ($@ ne '') {
  10064. my($err) = $@; chomp $err;
  10065. if ($err =~ /^LDAP_/) { # LDAP related error
  10066. do_log(0, "NOTICE: do_search: trying again: $err");
  10067. $self->disconnect_from_ldap;
  10068. $self->connect_to_ldap;
  10069. $self->ldap or die "do_search: reconnect failed";
  10070. do_log(5,sprintf(
  10071. "lookup_ldap: searching (again) base=\"%s\", scope=\"%s\", filter=\"%s\"", $base, $scope, $filter));
  10072. eval {
  10073. $result = $self->{ldap}->search(base => $base,
  10074. scope => $scope,
  10075. filter => $filter,
  10076. );
  10077. if ($result->code) { die $result->error_name, "\n"; }
  10078. };
  10079. if (@_ ne '') {
  10080. my($err) = $@; chomp $err;
  10081. $self->disconnect_from_ldap;
  10082. die "do_search: failed again, $err";
  10083. }
  10084. }
  10085. die "do_search: $err";
  10086. }
  10087. return $result;
  10088. }
  10089. 1;
  10090. #
  10091. package Amavis::Lookup::LDAPattr;
  10092. use strict;
  10093. use re 'taint';
  10094. BEGIN {
  10095. use Exporter ();
  10096. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  10097. $VERSION = '2.043';
  10098. @ISA = qw(Exporter);
  10099. import Amavis::Util qw(ll do_log)
  10100. }
  10101. # attrtype: B=boolean, N=numeric, S=string, L=list
  10102. # N-: numeric, nonexistent field returns undef without complaint
  10103. # S-: string, nonexistent field returns undef without complaint
  10104. # L-: list, nonexistent field returns undef without complaint
  10105. # B-: boolean, nonexistent field returns undef without complaint
  10106. # B0: boolean, nonexistent field treated as false
  10107. # B1: boolean, nonexistent field treated as true
  10108. sub new($$$;$) {
  10109. my($class,$ldap_query,$attrname,$attrtype) = @_;
  10110. return undef if !defined($ldap_query);
  10111. my($self) = bless {}, $class;
  10112. $self->{ldap_query} = $ldap_query;
  10113. $self->{attrname} = lc($attrname);
  10114. $self->{attrtype} = uc($attrtype);
  10115. $self;
  10116. }
  10117. sub lookup_ldap_attr($$$) {
  10118. my($self,$addr,$get_all) = @_;
  10119. my(@result,@matchingkey);
  10120. if (!defined($self)) {
  10121. do_log(5,"lookup_ldap_attr - undefined, \"$addr\" no match");
  10122. } elsif (!defined($self->{ldap_query})) {
  10123. do_log(5,sprintf("lookup_ldap_attr(%s) - null query, \"%s\" no match",
  10124. $self->{attrname}, $addr));
  10125. } else {
  10126. my($attr) = $self->{attrname};
  10127. my($res_ref,$mk_ref) = $self->{ldap_query}->lookup_ldap($addr,1);
  10128. do_log(5,"lookup_ldap_attr($attr), \"$addr\" no matching records")
  10129. if !defined($res_ref) || !@$res_ref;
  10130. for my $ind (0 .. (!defined($res_ref) ? -1 : $#$res_ref)) {
  10131. my($match); my($h_ref) = $res_ref->[$ind]; my($mk) = $mk_ref->[$ind];
  10132. if (!exists($h_ref->{$attr})) {
  10133. # record found, but no attribute with that name in the table
  10134. if ( $self->{attrtype} =~ /^B0/) { # boolean, defaults to false
  10135. $match = 0; # nonexistent attribute treated as 0
  10136. do_log(5,"lookup_ldap_attr($attr), no attribute, \"$addr\" result=$match");
  10137. } elsif ($self->{attrtype} =~ /^B1/) { # boolean, defaults to true
  10138. $match = 1; # nonexistent attribute treated as 1
  10139. do_log(5,"lookup_ldap_attr($attr), no attribute, \"$addr\" result=$match");
  10140. } elsif ($self->{attrtype}=~/^.-/s) { # allowed to not exist
  10141. do_log(5,"lookup_ldap_attr($attr), no attribute, \"$addr\" result=undef");
  10142. } else { # treated as 'no match', issue a warning
  10143. do_log(1,"lookup_ldap_attr($attr) ".
  10144. "(WARN: no such attribute in LDAP entry), ".
  10145. "\"$addr\" result=undef");
  10146. }
  10147. } else { # attribute exists
  10148. $match = $h_ref->{$attr};
  10149. if (!defined($match)) { # NULL attribute values represented as undef
  10150. } elsif ($self->{attrtype} =~ /^B/) { # boolean
  10151. $match = $match eq "TRUE" ? 1 : 0; # convert TRUE|FALSE to 1|0
  10152. } elsif ($self->{attrtype} =~ /^N/) { # numeric
  10153. $match = $match + 0; # unify different numeric forms
  10154. } elsif ($self->{attrtype} =~ /^S/) { # string
  10155. $match =~ s/ +\z//; # trim trailing spaces
  10156. } elsif ($self->{attrtype} =~ /^L/) { # list
  10157. #$match = join(", ",@$match);
  10158. }
  10159. do_log(5,sprintf("lookup_ldap_attr(%s) \"%s\" result=(%s)",
  10160. $attr, $addr, defined($match) ? $match : 'undef'));
  10161. }
  10162. if (defined $match) {
  10163. push(@result,$match); push(@matchingkey,$mk);
  10164. last if !$get_all;
  10165. }
  10166. }
  10167. }
  10168. if (!$get_all) { !wantarray ? $result[0] : ($result[0], $matchingkey[0]) }
  10169. else { !wantarray ? \@result : (\@result, \@matchingkey) }
  10170. }
  10171. 1;
  10172. #
  10173. package Amavis::Lookup::LDAP;
  10174. use strict;
  10175. use re 'taint';
  10176. BEGIN {
  10177. use Exporter ();
  10178. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION
  10179. $ldap_sys_default @ldap_attrs @mv_ldap_attrs);
  10180. $VERSION = '2.043';
  10181. @ISA = qw(Exporter);
  10182. import Amavis::Conf qw(:platform :confvars c cr ca);
  10183. import Amavis::Timing qw(section_time);
  10184. import Amavis::Util qw(untaint snmp_count ll do_log);
  10185. import Amavis::rfc2821_2822_Tools qw(make_query_keys split_address);
  10186. import Amavis::LDAP::Connection ();
  10187. $ldap_sys_default = {
  10188. base => undef,
  10189. scope => 'sub',
  10190. query_filter => '(&(objectClass=amavisAccount)(mail=%m))',
  10191. };
  10192. @ldap_attrs = qw(amavisVirusLover amavisSpamLover amavisBannedFilesLover
  10193. amavisBadHeaderLover amavisBypassVirusChecks amavisBypassSpamChecks
  10194. amavisBypassBannedChecks amavisBypassHeaderChecks amavisSpamTagLevel
  10195. amavisSpamTag2Level amavisSpamKillLevel amavisSpamModifiesSubj
  10196. amavisVirusQuarantineTo amavisSpamQuarantineTo amavisBannedQuarantineTo
  10197. amavisBadHeaderQuarantineTo amavisBlacklistSender amavisWhitelistSender
  10198. amavisLocal amavisMessageSizeLimit amavisWarnVirusRecip
  10199. amavisWarnBannedRecip amavisWarnBadHeaderRecip amavisVirusAdmin
  10200. amavisNewVirusAdmin amavisSpamAdmin amavisBannedAdmin
  10201. amavisBadHeaderAdmin amavisBannedRuleNames
  10202. );
  10203. @mv_ldap_attrs = qw(amavisBlacklistSender amavisWhitelistSender
  10204. amavisBannedRuleNames
  10205. );
  10206. }
  10207. sub new {
  10208. my($class,$default,$conn_h) = @_;
  10209. my($self) = bless {}, $class;
  10210. $self->{conn_h} = $conn_h;
  10211. $self->{incarnation} = 0;
  10212. for (qw(base scope query_filter)) {
  10213. # replace undefined attributes with config values or defaults
  10214. $self->{$_} = $default->{$_} unless defined($self->{$_});
  10215. $self->{$_} = $ldap_sys_default->{$_} unless defined($self->{$_});
  10216. }
  10217. $self;
  10218. }
  10219. sub DESTROY {
  10220. my($self) = shift;
  10221. eval { do_log(5,"Amavis::Lookup::LDAP DESTROY called") };
  10222. }
  10223. sub init {
  10224. my($self) = @_;
  10225. if ($self->{incarnation} != $self->{conn_h}->incarnation) { # invalidated?
  10226. $self->{incarnation} = $self->{conn_h}->incarnation;
  10227. $self->clear_cache; # db handle has changed, invalidate cache
  10228. }
  10229. $self;
  10230. }
  10231. sub clear_cache {
  10232. my($self) = @_;
  10233. delete $self->{cache};
  10234. }
  10235. sub lookup_ldap($$$) {
  10236. my($self,$addr,$get_all) = @_;
  10237. my(@result,@matchingkey,@tmp_result,@tmp_matchingkey);
  10238. if (exists $self->{cache} && exists $self->{cache}->{$addr}) { # cached?
  10239. my($c) = $self->{cache}->{$addr}; @result = @$c if ref $c;
  10240. @matchingkey = map {'/cached/'} @result; # will do for now, improve some day
  10241. # if (!ll(5)) {
  10242. # # don't bother preparing log report which will not be printed
  10243. # } elsif (!@result) {
  10244. # do_log(5,"lookup_ldap (cached): \"$addr\" no match");
  10245. # } else {
  10246. # for my $m (@result) {
  10247. # do_log(5, sprintf("lookup_ldap (cached): \"%s\" matches, result=(%s)",
  10248. # $addr, join(", ", map { sprintf("%s=>%s", $_,
  10249. # !defined($m->{$_})?'-':'"'.$m->{$_}.'"'
  10250. # ) } sort keys(%$m) ) ));
  10251. # }
  10252. # }
  10253. if (!$get_all) {
  10254. return(!wantarray ? $result[0] : ($result[0], $matchingkey[0]));
  10255. } else {
  10256. return(!wantarray ? \@result : (\@result, \@matchingkey));
  10257. }
  10258. }
  10259. my($is_local); # LDAP is not looked up to avoid recursion!
  10260. $is_local = Amavis::Lookup::lookup(0,$addr,
  10261. grep {ref ne 'Amavis::Lookup::SQL' &&
  10262. ref ne 'Amavis::Lookup::SQLfield' &&
  10263. ref ne 'Amavis::Lookup::LDAP' &&
  10264. ref ne 'Amavis::Lookup::LDAPattr'}
  10265. @{ca('local_domains_maps')});
  10266. my($keys_ref,$rhs_ref,@keys);
  10267. ($keys_ref,$rhs_ref) = make_query_keys($addr,0,$is_local);
  10268. @keys = @$keys_ref;
  10269. unshift(@keys, '<>') if $addr eq ''; # a hack for a null return path
  10270. $_ = untaint($_) for @keys; # untaint keys
  10271. $_ = Net::LDAP::Util::escape_filter_value($_) for @keys;
  10272. # process %m
  10273. my @filter_attr;
  10274. my $filter = $self->{query_filter};
  10275. while ($filter =~ /%m/) {
  10276. (my $filter_pair) = $filter =~ /\(([^(]*=%m)\)/;
  10277. my ($filter_attr) = split(/=/, $filter_pair);
  10278. my $filter_string = '|' . join('', map { "($filter_attr=$_)" } @keys);
  10279. $filter =~ s/\Q$filter_pair\E/$filter_string/;
  10280. push(@filter_attr, $filter_attr);
  10281. }
  10282. # process %d
  10283. my($base) = $self->{base};
  10284. if ($base =~ /%d/) {
  10285. my($localpart,$domain) = split_address($addr);
  10286. if ($domain) {
  10287. $domain = untaint($domain); $domain = lc($domain);
  10288. $domain =~ s/^\@?(.*?)\.*\z/$1/s;
  10289. $base =~ s/%d/&Net::LDAP::Util::escape_dn_value($domain)/ge;
  10290. }
  10291. }
  10292. # build hash of keys and array position
  10293. my(%xref,$key_num);
  10294. $xref{$_} = $key_num++ for @keys;
  10295. #
  10296. do_log(4,sprintf("lookup_ldap \"%s\", query keys: %s, base: %s, filter: %s",
  10297. $addr,join(', ',map{"\"$_\""}@keys),$self->{base},$self->{query_filter}));
  10298. my($conn_h) = $self->{conn_h};
  10299. $conn_h->begin_work; # (re)connect if not connected
  10300. eval {
  10301. snmp_count('OpsLDAPSearch');
  10302. my($result) = $conn_h->do_search($base, $self->{scope}, $filter );
  10303. my(@entry) = $result->entries;
  10304. for my $entry (@entry) {
  10305. my($match) = {};
  10306. $match->{dn} = $entry->dn;
  10307. for my $attr (@ldap_attrs) {
  10308. my($value);
  10309. $attr = lc($attr);
  10310. do_log(9,"lookup_ldap: reading attribute \"$attr\" from object");
  10311. if (grep /^$attr\z/i, @mv_ldap_attrs) { # multivalued
  10312. $value = $entry->get_value($attr, asref => 1);
  10313. } else {
  10314. $value = $entry->get_value($attr);
  10315. }
  10316. $match->{$attr} = $value if $value;
  10317. }
  10318. my $pos;
  10319. for my $attr (@filter_attr) {
  10320. my $value = $entry->get_value($attr);
  10321. if ($value) {
  10322. if (!exists $match->{'amavislocal'} && $value eq '@.') {
  10323. # NOTE: see lookup_sql
  10324. $match->{'amavislocal'} = undef;
  10325. do_log(5,
  10326. "lookup_ldap: \"$addr\" matches catchall, amavislocal=>undef");
  10327. }
  10328. $pos = $xref{$value};
  10329. last;
  10330. }
  10331. }
  10332. my $key_str = join(", ",map {sprintf("%s=>%s",$_,!defined($match->{$_})?
  10333. '-':'"'.$match->{$_}.'"')} keys(%$match));
  10334. push(@tmp_result, [$pos,{%$match}]); # copy hash
  10335. push(@tmp_matchingkey, [$pos,$key_str]);
  10336. last if !$get_all;
  10337. }
  10338. }; # eval
  10339. if ($@ ne '') {
  10340. my($err) = $@; chomp $err;
  10341. do_log(-1,"lookup_ldap: $err");
  10342. die $err;
  10343. }
  10344. @result = map {$_->[1]} sort {$a->[0] <=> $b->[0]} @tmp_result;
  10345. @matchingkey = map {$_->[1]} sort {$a->[0] <=> $b->[0]} @tmp_matchingkey;
  10346. if (!ll(4)) {
  10347. # don't bother preparing log report which will not be printed
  10348. } elsif (!@result) {
  10349. do_log(4,"lookup_ldap, \"$addr\" no match")
  10350. } else {
  10351. do_log(4,"lookup_ldap($addr) matches, result=($_)") for @matchingkey;
  10352. }
  10353. # save for future use, but only within processing of this message
  10354. $self->{cache}->{$addr} = \@result;
  10355. section_time('lookup_ldap');
  10356. if (!$get_all) { !wantarray ? $result[0] : ($result[0], $matchingkey[0]) }
  10357. else { !wantarray ? \@result : (\@result, \@matchingkey) }
  10358. }
  10359. 1;
  10360. __DATA__
  10361. #
  10362. package Amavis::In::AMCL;
  10363. use strict;
  10364. use re 'taint';
  10365. BEGIN {
  10366. use Exporter ();
  10367. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  10368. $VERSION = '2.043';
  10369. @ISA = qw(Exporter);
  10370. }
  10371. use subs @EXPORT;
  10372. use Errno qw(ENOENT EACCES);
  10373. use IO::File ();
  10374. use Digest::MD5;
  10375. BEGIN {
  10376. import Amavis::Conf qw(:platform :confvars c cr ca);
  10377. import Amavis::Util qw(ll do_log debug_oneshot snmp_counters_init snmp_count
  10378. am_id new_am_id untaint rmdir_recursively add_entropy);
  10379. import Amavis::Lookup qw(lookup);
  10380. import Amavis::Lookup::IP qw(lookup_ip_acl);
  10381. import Amavis::Timing qw(section_time);
  10382. import Amavis::rfc2821_2822_Tools;
  10383. import Amavis::In::Message;
  10384. import Amavis::In::Connection;
  10385. import Amavis::IO::Zlib;
  10386. import Amavis::Out::EditHeader qw(hdr);
  10387. import Amavis::Out qw(mail_dispatch);
  10388. import Amavis::Notify qw(msg_from_quarantine);
  10389. }
  10390. sub new($) { my($class) = @_; bless {}, $class }
  10391. # used with sendmail milter and traditional (non-SMTP) MTA interface,
  10392. # but also to request a message release from a quarantine
  10393. #
  10394. sub process_policy_request($$$$) {
  10395. my($self, $sock, $conn, $check_mail, $old_amcl) = @_;
  10396. # $sock: connected socket from Net::Server
  10397. # $conn: information about client connection
  10398. # $check_mail: subroutine ref to be called with file handle
  10399. my(%attr);
  10400. $0 = sprintf("amavisd (ch%d-P-idle)", $Amavis::child_invocation_count);
  10401. do_log(5, "process_policy_request: $old_amcl, $0");
  10402. if ($old_amcl) {
  10403. # Accept a single request from traditional amavis helper program.
  10404. # Receive TEMPDIR/SENDER/RCPTS/LDA/LDAARGS from client
  10405. # Simple protocol: \2 means LDA follows; \3 means EOT (end of transmission)
  10406. my($state) = 0; $attr{'request'} = 'AM.CL'; my($response) = "\001";
  10407. my($rv,@recips,@ldaargs,$inbuff); local($1);
  10408. my(@attr_names) = qw(tempdir sender recipient ldaargs);
  10409. while (defined($rv = recv($sock, $inbuff, 8192, 0))) {
  10410. $0 = sprintf("amavisd (ch%d-P)", $Amavis::child_invocation_count);
  10411. if ($state < 2) {
  10412. $attr{$attr_names[$state]} = $inbuff; $state++;
  10413. } elsif ($state == 2 && $inbuff eq "\002") {
  10414. $state++;
  10415. } elsif ($state >= 2 && $inbuff eq "\003") {
  10416. section_time('got data');
  10417. $attr{'recipient'} = \@recips; $attr{'ldaargs'} = \@ldaargs;
  10418. $attr{'delivery_care_of'} = @ldaargs ? 'client' : 'server';
  10419. eval {
  10420. my($msginfo) = preprocess_policy_query(\%attr);
  10421. $response = (map { /^exit_code=(\d+)\z/ ? $1 : () }
  10422. check_amcl_policy($conn,$msginfo,$check_mail,1))[0];
  10423. };
  10424. if ($@ ne '') {
  10425. chomp($@); do_log(-2, "policy_server FAILED: $@");
  10426. $response = EX_TEMPFAIL;
  10427. }
  10428. $state = 4;
  10429. } elsif ($state == 2) {
  10430. push(@recips, $inbuff);
  10431. } else {
  10432. push(@ldaargs, $inbuff);
  10433. }
  10434. defined send($sock,$response,0) or die "send failed in state $state: $!";
  10435. last if $state >= 4;
  10436. $0 = sprintf("amavisd (ch%d-P-idle)", $Amavis::child_invocation_count);
  10437. }
  10438. if ($state==4 && defined($rv)) {
  10439. # normal termination
  10440. } elsif (!defined($rv) && $! != 0) {
  10441. die "recv failed in state $state: $!";
  10442. } else { # eof or a runaway state
  10443. die "helper client session terminated unexpectedly, state: $state";
  10444. }
  10445. do_log(2, Amavis::Timing::report()); # report elapsed times
  10446. } else { # new amavis helper protocol AM.PDP or a Postfix policy server
  10447. # for Postfix policy server see Postfix docs SMTPD_POLICY_README
  10448. my(@response); local($1,$2,$3);
  10449. local($/) = "\012"; # set line terminator to LF (Postfix idiosyncrasy)
  10450. my($ln); # can accept multiple tasks
  10451. for (undef $!; defined($ln=$sock->getline); undef $!) {
  10452. $0 = sprintf("amavisd (ch%d-P)", $Amavis::child_invocation_count);
  10453. Amavis::Timing::init(); snmp_counters_init();
  10454. # must not use \r and \n, not \015 and \012 on certain platforms
  10455. if ($ln =~ /^\015?\012\z/) { # end of request
  10456. section_time('got data');
  10457. eval {
  10458. my($msginfo) = preprocess_policy_query(\%attr);
  10459. @response = $attr{'request'} eq 'smtpd_access_policy'
  10460. ? postfix_policy($conn,$msginfo,\%attr)
  10461. : $attr{'request'} eq 'release'
  10462. ? dispatch_from_quarantine($conn,$msginfo)
  10463. : check_amcl_policy($conn,$msginfo,$check_mail,0);
  10464. };
  10465. if ($@ ne '') {
  10466. chomp($@); do_log(-2, "policy_server FAILED: $@");
  10467. @response = (proto_encode('setreply','450','4.5.0',"Failure: $@"),
  10468. proto_encode('return_value','tempfail'),
  10469. proto_encode('exit_code',sprintf("%d",EX_TEMPFAIL)));
  10470. # last;
  10471. }
  10472. $sock->print( map { $_."\015\012" } (@response,'') )
  10473. or die "Can't write response to socket: $!";
  10474. %attr = (); @response = ();
  10475. do_log(2, Amavis::Timing::report());
  10476. } elsif ($ln =~ /^ ([^=\000\012]*?) (=|:[ \t]*)
  10477. ([^\012]*?) \015?\012 \z/xsi) {
  10478. my($attr_name) = Amavis::tcp_lookup_decode($1);
  10479. my($attr_val) = Amavis::tcp_lookup_decode($3);
  10480. if (!exists $attr{$attr_name}) {
  10481. $attr{$attr_name} = $attr_val;
  10482. } else {
  10483. $attr{$attr_name} = [ $attr{$attr_name} ] if !ref $attr{$attr_name};
  10484. push(@{$attr{$attr_name}}, $attr_val);
  10485. }
  10486. my($known_attr) = scalar(grep {$_ eq $attr_name} qw(
  10487. request helo_name protocol_state protocol_name queue_id
  10488. client_name client_address sender recipient
  10489. mail_id secret_id quar_type mail_file) );
  10490. do_log(!$known_attr?-1:1, "policy protocol: $attr_name=$attr_val");
  10491. } else {
  10492. do_log(-1, "policy protocol: INVALID ATTRIBUTE LINE: $ln");
  10493. }
  10494. $0 = sprintf("amavisd (ch%d-P-idle)", $Amavis::child_invocation_count);
  10495. }
  10496. defined $ln || $!==0 or die "Read from client socket FAILED: $!";
  10497. };
  10498. $0 = sprintf("amavisd (ch%d-P)", $Amavis::child_invocation_count);
  10499. }
  10500. # Based on given policy query attributes describing message to be cached
  10501. # or released, return a new Amavis::In::Message object
  10502. #
  10503. sub preprocess_policy_query($) {
  10504. my($attr_ref) = @_;
  10505. my($msginfo) = Amavis::In::Message->new;
  10506. $msginfo->rx_time(time); # now
  10507. add_entropy(%$attr_ref);
  10508. # amavisd -> amavis-helper protocol query consists of any number of
  10509. # the following lines, the response is terminated by an empty line.
  10510. # The 'request=AM.PDP' is a required first field, the order of
  10511. # remaining fields is arbitrary, but multivalued attributes such as
  10512. # 'recipient' must retain their relative order.
  10513. # Required AM.PDP fields are: request, tempdir, sender, recipient(s)
  10514. # request=AM.PDP
  10515. # tempdir=/var/amavis/amavis-milter-MWZmu9Di
  10516. # tempdir_removed_by=client (tempdir_removed_by=server is a default)
  10517. # mail_file=/var/amavis/am.../email.txt (defaults to tempdir/email.txt)
  10518. # sender=<foo@example.com>
  10519. # recipient=<bar1@example.net>
  10520. # recipient=<bar2@example.net>
  10521. # recipient=<bar3@example.net>
  10522. # delivery_care_of=server (client or server, client is a default)
  10523. # queue_id=qid
  10524. # protocol_name=ESMTP
  10525. # helo_name=b.example.com
  10526. # client_address=10.2.3.4
  10527. # Required 'release' fields are: request, mail_id
  10528. # request=release
  10529. # mail_id=xxxxxxxxxxxx
  10530. # secret_id=xxxxxxxxxxxx (authorizes a release)
  10531. # quar_type=x F/Z/B/Q/M (defaults to Q or F)
  10532. # file/zipfile/bsmtp/sql/mailbox
  10533. # mail_file=... (optional: overrides automatics; $QUARANTINEDIR prepended)
  10534. # requested_by=<releaser@example.com> (optional: lands in Resent-From:)
  10535. # sender=<foo@example.com> (optional: replaces envelope sender)
  10536. # recipient=<bar1@example.net> (optional: replaces envelope recips)
  10537. # recipient=<bar2@example.net>
  10538. # recipient=<bar3@example.net>
  10539. my($sender,@recips);
  10540. exists $attr_ref->{'request'} or die "Missing 'request' field";
  10541. my($ampdp) = $attr_ref->{'request'} =~ /^AM\.CL|AM\.PDP|release\z/i;
  10542. $msginfo->delivery_method(
  10543. lc($attr_ref->{'delivery_care_of'}) eq 'server' ? c('forward_method') :'');
  10544. $msginfo->client_delete(lc($attr_ref->{'tempdir_removed_by'}) eq 'client'
  10545. ? 1 : 0);
  10546. $msginfo->queue_id($attr_ref->{'queue_id'})
  10547. if exists $attr_ref->{'queue_id'};
  10548. $msginfo->client_addr($attr_ref->{'client_address'})
  10549. if exists $attr_ref->{'client_address'};
  10550. $msginfo->client_name($attr_ref->{'client_name'})
  10551. if exists $attr_ref->{'client_name'};
  10552. $msginfo->client_proto($attr_ref->{'protocol_name'})
  10553. if exists $attr_ref->{'protocol_name'};
  10554. $msginfo->client_helo($attr_ref->{'helo_name'})
  10555. if exists $attr_ref->{'helo_name'};
  10556. # $msginfo->body_type('8BITMIME'); # get_body_digest will set this if undef
  10557. $msginfo->requested_by(unquote_rfc2821_local($attr_ref->{'requested_by'}))
  10558. if exists $attr_ref->{'requested_by'};
  10559. if (exists $attr_ref->{'sender'}) {
  10560. $sender = $attr_ref->{'sender'};
  10561. $sender = unquote_rfc2821_local($sender);
  10562. $msginfo->sender($sender);
  10563. }
  10564. if (exists $attr_ref->{'recipient'}) {
  10565. my($r) = $attr_ref->{'recipient'};
  10566. @recips = !ref($r) ? $r : @$r;
  10567. $_ = unquote_rfc2821_local($_) for @recips;
  10568. $msginfo->recips(\@recips);
  10569. }
  10570. if (!exists $attr_ref->{'tempdir'}) {
  10571. $msginfo->mail_tempdir($TEMPBASE); # defaults to $TEMPBASE
  10572. } else {
  10573. local($1,$2); my($tempdir) = $attr_ref->{tempdir};
  10574. $tempdir =~ /^ (?: \Q$TEMPBASE\E | \Q$MYHOME\E )
  10575. \/ (?! \.{1,2} \z) [A-Za-z0-9_.-]+ \z/xso
  10576. or die "Invalid/unexpected temporary directory name '$tempdir'";
  10577. $msginfo->mail_tempdir(untaint($tempdir));
  10578. }
  10579. my($quar_type);
  10580. if (!$ampdp) {} # don't bother with filenames
  10581. elsif ($attr_ref->{'request'} eq 'release') {
  10582. exists $attr_ref->{'mail_id'} or die "Missing 'mail_id' field";
  10583. my($fn) = $attr_ref->{'mail_id'};
  10584. $fn =~ m{^[A-Za-z0-9][A-Za-z0-9/_.+-]*\z}s or die "Invalid mail_id '$fn'";
  10585. $msginfo->mail_id($fn);
  10586. if (!exists($attr_ref->{'secret_id'}) || $attr_ref->{'secret_id'} eq '') {
  10587. die "Secret_id is required, but missing" if c('auth_required_release');
  10588. } else {
  10589. my($id) = Digest::MD5->new->add($attr_ref->{'secret_id'})->b64digest;
  10590. $id = substr($id,0,12); $id =~ tr{/}{-};
  10591. $id eq $fn or die "Result $id of secret_id does not match mail_id $fn";
  10592. }
  10593. $quar_type = $attr_ref->{'quar_type'};
  10594. if ($quar_type eq '') # choose some reasonable default (simpleminded)
  10595. { $quar_type = c('spam_quarantine_method') =~ /^sql:/i ? 'Q' : 'F' }
  10596. if ($quar_type eq 'F' || $quar_type eq 'Z') {
  10597. $QUARANTINEDIR ne '' or die "Config variable \$QUARANTINEDIR is empty";
  10598. if ($attr_ref->{'mail_file'} ne '') {
  10599. $fn = $attr_ref->{'mail_file'};
  10600. $fn =~ m{^[A-Za-z0-9][A-Za-z0-9/_.+-]*\z}s && $fn !~ m{\.\./}
  10601. or die "Unsafe filename '$fn'";
  10602. $fn = $QUARANTINEDIR.'/'.untaint($fn);
  10603. } else { # automatically guess a filename - simpleminded
  10604. if ($quarantine_subdir_levels < 1) { $fn = "$QUARANTINEDIR/$fn" }
  10605. else { my($subd) = substr($fn,0,1); $fn = "$QUARANTINEDIR/$subd/$fn" }
  10606. $fn .= '.gz' if $quar_type eq 'Z';
  10607. }
  10608. }
  10609. $msginfo->mail_text_fn($fn);
  10610. } elsif (!exists $attr_ref->{'mail_file'}) {
  10611. $msginfo->mail_text_fn($msginfo->mail_tempdir . '/email.txt');
  10612. } else {
  10613. # SECURITY: just believe the supplied file name, blindly untainting it
  10614. $msginfo->mail_text_fn(untaint($attr_ref->{'mail_file'}));
  10615. }
  10616. if ($ampdp && $msginfo->mail_text_fn ne '') {
  10617. my($fh); my($fname) = $msginfo->mail_text_fn;
  10618. new_am_id('rel-'.$msginfo->mail_id) if $attr_ref->{'request'} eq 'release';
  10619. if ($attr_ref->{'request'} eq 'release' && $quar_type eq 'Q') {
  10620. do_log(5, "preprocess_policy_query: opening in sql: ".$msginfo->mail_id);
  10621. my($obj) = $Amavis::sql_storage;
  10622. $Amavis::extra_code_sql_quar && $obj
  10623. or die "SQL quarantine code not enabled";
  10624. my($conn_h) = $obj->{conn_h}; my($sql_cl_r) = cr('sql_clause');
  10625. $conn_h->begin_work_nontransaction; # (re)connect if not connected
  10626. $fh = Amavis::IO::SQL->new;
  10627. $fh->open($conn_h,$sql_cl_r->{'sel_quar'},untaint($msginfo->mail_id))
  10628. or die "Can't open sql obj for reading: $!";
  10629. } else {
  10630. do_log(5, "preprocess_policy_query: opening mail '$fname'");
  10631. # set new amavis message id
  10632. new_am_id( ($fname =~ m{amavis-(milter-)?([^/ \t]+)}s ? $2 : undef) )
  10633. if $attr_ref->{'request'} ne 'release';
  10634. # file created by amavis helper program or other client, just open it
  10635. my(@stat_list) = lstat($fname); my($errn) = @stat_list ? 0 : 0+$!;
  10636. if ($errn == ENOENT) { die "File $fname does not exist" }
  10637. elsif ($errn) { die "File $fname inaccessible: $!" }
  10638. elsif (!-f _) { die "File $fname is not a plain file" }
  10639. add_entropy(@stat_list);
  10640. if ($fname =~ /\.gz\z/) {
  10641. $fh = Amavis::IO::Zlib->new;
  10642. $fh->open($fname,'rb') or die "Can't open gzipped file $fname: $!";
  10643. } else {
  10644. $msginfo->msg_size(-s _);
  10645. $fh = IO::File->new;
  10646. $fh->open($fname,'<') or die "Can't open file $fname: $!";
  10647. binmode($fh,":bytes") or die "Can't cancel :utf8 mode: $!"
  10648. if $unicode_aware;
  10649. }
  10650. }
  10651. $msginfo->mail_text($fh); # save file handle to object
  10652. }
  10653. if ($ampdp) {
  10654. do_log(1, sprintf("%s %s %s: <%s> -> %s",
  10655. $attr_ref->{'request'}, $attr_ref->{'mail_id'},
  10656. $msginfo->mail_tempdir, $sender,
  10657. join(',', qquote_rfc2821_local(@recips)) ));
  10658. } else {
  10659. do_log(1, sprintf("Request: %s(%s): %s %s %s: %s[%s] <%s> -> <%s>",
  10660. @$attr_ref{qw(request protocol_state mail_id protocol_name
  10661. queue_id client_name client_address sender recipient)}));
  10662. }
  10663. $msginfo;
  10664. }
  10665. sub check_amcl_policy($$$$) {
  10666. my($conn,$msginfo,$check_mail,$old_amcl) = @_;
  10667. my($smtp_resp, $exit_code, $preserve_evidence);
  10668. my(%baseline_policy_bank); my($policy_changed) = 0;
  10669. %baseline_policy_bank = %current_policy_bank;
  10670. # do some sanity checks before deciding to call check_mail()
  10671. if (!ref($msginfo->per_recip_data) || !defined($msginfo->mail_text)) {
  10672. $smtp_resp = '450 4.5.0 Incomplete request'; $exit_code = EX_TEMPFAIL;
  10673. } else {
  10674. my($cl_ip) = $msginfo->client_addr; my($sender) = $msginfo->sender;
  10675. if ($cl_ip ne '' && defined $policy_bank{'MYNETS'}
  10676. && lookup_ip_acl($cl_ip,@{ca('mynetworks_maps')}) ) {
  10677. Amavis::load_policy_bank('MYNETS'); $policy_changed = 1;
  10678. }
  10679. if ($sender ne '' && defined $policy_bank{'MYUSERS'}
  10680. && lookup(0,$sender,@{ca('local_domains_maps')})) {
  10681. Amavis::load_policy_bank('MYUSERS'); $policy_changed = 1;
  10682. }
  10683. debug_oneshot(1) if lookup(0,$sender,@{ca('debug_sender_maps')});
  10684. # check_mail() expects open file on $fh, need not be rewound
  10685. Amavis::check_mail_begin_task();
  10686. ($smtp_resp, $exit_code, $preserve_evidence) =
  10687. &$check_mail($conn,$msginfo,0);
  10688. my($fh) = $msginfo->mail_text; my($tempdir) = $msginfo->mail_tempdir;
  10689. $fh->close or die "Error closing temp file: $!" if $fh;
  10690. $fh = undef; $msginfo->mail_text(undef);
  10691. my($errn) = $tempdir eq '' ? ENOENT : (stat($tempdir) ? 0 : 0+$!);
  10692. if ($tempdir eq '' || $errn == ENOENT) {
  10693. # do nothing
  10694. } elsif ($msginfo->client_delete) {
  10695. do_log(4, "AM.PDP: deletion of $tempdir is client's responsibility");
  10696. } elsif ($preserve_evidence) {
  10697. do_log(-1,"AM.PDP: tempdir is to be PRESERVED: $tempdir");
  10698. } else {
  10699. my($fname) = $msginfo->mail_text_fn;
  10700. do_log(4, "AM.PDP: tempdir and file being removed: $tempdir, $fname");
  10701. unlink($fname) or die "Can't remove file $fname: $!" if $fname ne '';
  10702. rmdir_recursively($tempdir);
  10703. }
  10704. }
  10705. # amavisd -> amavis-helper protocol response consists of any number of
  10706. # the following lines, the response is terminated by an empty line
  10707. # addrcpt=recipient
  10708. # delrcpt=recipient
  10709. # addheader=hdr_head hdr_body
  10710. # chgheader=index hdr_head hdr_body
  10711. # delheader=index hdr_head
  10712. # replacebody=new_body (not implemented)
  10713. # return_value=continue|reject|discard|accept|tempfail
  10714. # setreply=rcode xcode message
  10715. # exit_code=n
  10716. my(@response); my($rcpt_deletes,$rcpt_count)=(0,0);
  10717. if (ref($msginfo->per_recip_data)) {
  10718. for my $r (@{$msginfo->per_recip_data})
  10719. { $rcpt_count++; if ($r->recip_done) { $rcpt_deletes++ } }
  10720. }
  10721. local($1,$2,$3);
  10722. if ($smtp_resp=~/^([1-5]\d\d) ([245]\.\d{1,3}\.\d{1,3})(?: |\z)(.*)\z/s)
  10723. { push(@response, proto_encode('setreply', $1,$2,$3)) }
  10724. if ( $exit_code == EX_TEMPFAIL) {
  10725. push(@response, proto_encode('return_value','tempfail'));
  10726. } elsif ($exit_code == EX_NOUSER) { # reject the whole message
  10727. push(@response, proto_encode('return_value','reject'));
  10728. } elsif ($exit_code == EX_UNAVAILABLE) { # reject the whole message
  10729. push(@response, proto_encode('return_value','reject'));
  10730. } elsif ($exit_code == 99) { # discard the whole message
  10731. push(@response, proto_encode('return_value','discard'));
  10732. } elsif ($msginfo->delivery_method ne '') { # explicit forwarding by server
  10733. $rcpt_count==$rcpt_deletes or die "Not all recips done"; # just in case
  10734. # MTA is relieved of duty to deliver a message, amavisd did the forwarding
  10735. $exit_code = EX_OK; # *** 99 or EX_OK; ??? (doesn't really matter with
  10736. # helper client programs which can't do the delivery)
  10737. push(@response, proto_encode('return_value','continue')); # 'discard' ???
  10738. } elsif ($rcpt_count-$rcpt_deletes <= 0) { # none left, should be discarded
  10739. # discarding could have been requested (?)
  10740. do_log(-1, "WARN: no recips left (forgot to set ".
  10741. "\$forward_method=undef using milter?), $smtp_resp");
  10742. $exit_code = 99;
  10743. push(@response, proto_encode('return_value','discard'));
  10744. } else { # EX_OK
  10745. for my $r (@{$msginfo->per_recip_data}) { # modified recipient addresses?
  10746. my($addr,$newaddr) = ($r->recip_addr, $r->recip_final_addr);
  10747. if ($r->recip_done) { # delete
  10748. push(@response, proto_encode('delrcpt',
  10749. quote_rfc2821_local($addr)));
  10750. } elsif ($newaddr ne $addr) { # modify, e.g. adding extension
  10751. push(@response, proto_encode('delrcpt',
  10752. quote_rfc2821_local($addr)));
  10753. push(@response, proto_encode('addrcpt',
  10754. quote_rfc2821_local($newaddr)));
  10755. }
  10756. }
  10757. my($hdr_edits) = $msginfo->header_edits;
  10758. if ($hdr_edits) { # any added or modified header fields?
  10759. local($1,$2);
  10760. # Inserting. Not posible to specify placement of header fields in milter!
  10761. for my $hf (@{$hdr_edits->{prepend}}, @{$hdr_edits->{append}}) {
  10762. if ($hf =~ /^([^:]+):[ \t]*(.*?)$/s)
  10763. { push(@response, proto_encode('addheader',$1,$2)) }
  10764. }
  10765. my($field_name,$edit,$field_body);
  10766. while ( ($field_name,$edit) = each %{$hdr_edits->{edit}} ) {
  10767. $field_body = $msginfo->mime_entity->head->get($field_name,0);
  10768. if (!defined($field_body)) {
  10769. # such header field does not exist, do nothing
  10770. } elsif (!defined($edit)) { # delete existing header field
  10771. push(@response, proto_encode('delheader',"1",$field_name));
  10772. } else { # edit the first occurrence
  10773. chomp($field_body);
  10774. $field_body = hdr($field_name, &$edit($field_name,$field_body));
  10775. $field_body = $1 if $field_body =~ /^[^:]+:[ \t]*(.*?)$/s;
  10776. push(@response, proto_encode('chgheader', "1",
  10777. $field_name, $field_body));
  10778. }
  10779. }
  10780. }
  10781. if ($old_amcl) { # milter via old amavis helper program
  10782. # warn if there is anything that should be done but MTA is not capable of
  10783. # (or a helper program can not pass the request)
  10784. for (grep { /^(delrcpt|addrcpt)=/ } @response)
  10785. { do_log(-1, "WARN: MTA can't do: $_") }
  10786. if ($rcpt_deletes && $rcpt_count-$rcpt_deletes > 0) {
  10787. do_log(-1, "WARN: ACCEPT THE WHOLE MESSAGE, ".
  10788. "MTA-in can't do selective recips deletion");
  10789. }
  10790. }
  10791. push(@response, proto_encode('return_value','continue'));
  10792. }
  10793. push(@response, proto_encode('exit_code',sprintf("%d",$exit_code)));
  10794. ll(2) && do_log(2, "mail checking ended: ".join("\n",@response));
  10795. if ($policy_changed) {
  10796. %current_policy_bank = %baseline_policy_bank; $policy_changed = 0;
  10797. }
  10798. @response;
  10799. }
  10800. sub postfix_policy($$$) {
  10801. my($conn,$msginfo,$attr_ref) = @_;
  10802. my(@response);
  10803. if ($attr_ref->{'request'} ne 'smtpd_access_policy') {
  10804. die ("unknown 'request' value: " . $attr_ref->{'request'});
  10805. } else {
  10806. @response = 'action=DUNNO';
  10807. }
  10808. @response;
  10809. }
  10810. sub proto_encode($@) {
  10811. my($attribute_name,@strings) = @_; local($1);
  10812. $attribute_name =~ # encode all but alfanumerics, '_' and '-'
  10813. s/([^0-9a-zA-Z_-])/sprintf("%%%02x",ord($1))/eg;
  10814. for (@strings) { # encode % and nonprintables
  10815. s/([^\041-\044\046-\176])/sprintf("%%%02x",ord($1))/eg;
  10816. }
  10817. $attribute_name . '=' . join(' ',@strings);
  10818. }
  10819. sub dispatch_from_quarantine($$) {
  10820. my($conn,$msginfo) = @_;
  10821. eval {
  10822. msg_from_quarantine($conn,$msginfo); # fill message object information
  10823. mail_dispatch($conn,$msginfo,1,1); # re-send the mail
  10824. };
  10825. my($err) = $@; chomp($err);
  10826. if ($@ ne '') { do_log(0, "WARN: dispatch_from_quarantine failed: $err") }
  10827. my(@response);
  10828. for my $r (@{$msginfo->per_recip_data}) {
  10829. local($1,$2,$3); my($smtp_s,$smtp_es,$msg);
  10830. my($resp) = $r->recip_smtp_response;
  10831. if ($err ne '')
  10832. { ($smtp_s,$smtp_es,$msg) = ('450', '4.5.0', "ERROR: $err") }
  10833. elsif ($resp =~ /^([1-5]\d\d) ([245]\.\d{1,3}\.\d{1,3})(?: |\z)(.*)\z/s)
  10834. { ($smtp_s,$smtp_es,$msg) = ($1,$2,$3) }
  10835. else
  10836. { ($smtp_s,$smtp_es,$msg) = ('450', '4.5.0', "Unexpected: $resp") }
  10837. push(@response, proto_encode('setreply',$smtp_s,$smtp_es,$msg));
  10838. }
  10839. @response;
  10840. }
  10841. 1;
  10842. __DATA__
  10843. #
  10844. package Amavis::In::SMTP;
  10845. use strict;
  10846. use re 'taint';
  10847. BEGIN {
  10848. use Exporter ();
  10849. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  10850. $VERSION = '2.043';
  10851. @ISA = qw(Exporter);
  10852. }
  10853. use Errno qw(ENOENT EACCES);
  10854. use MIME::Base64;
  10855. BEGIN {
  10856. import Amavis::Conf qw(:platform :confvars c cr ca);
  10857. import Amavis::Util qw(ll do_log am_id new_am_id snmp_counters_init
  10858. prolong_timer debug_oneshot sanitize_str
  10859. strip_tempdir rmdir_recursively add_entropy);
  10860. import Amavis::Lookup qw(lookup);
  10861. import Amavis::Lookup::IP qw(lookup_ip_acl);
  10862. import Amavis::Timing qw(section_time);
  10863. import Amavis::rfc2821_2822_Tools;
  10864. import Amavis::In::Message;
  10865. import Amavis::In::Connection;
  10866. }
  10867. sub new($) {
  10868. my($class) = @_;
  10869. my($self) = bless {}, $class;
  10870. $self->{sock} = undef; # SMTP socket
  10871. $self->{proto} = undef; # SMTP / ((ESMTP / LMTP) (A | S | SA)? )
  10872. $self->{pipelining} = undef; # may we buffer responses?
  10873. $self->{smtp_outbuf} = undef; # SMTP responses buffer for PIPELINING
  10874. $self->{fh_pers} = undef; # persistent file handle for email.txt
  10875. $self->{tempdir_persistent} = undef;# temporary directory for check_mail
  10876. $self->{preserve} = undef; # don't delete tempdir on exit
  10877. $self->{tempdir_empty} = 1; # anything of interest in tempdir?
  10878. $self->{session_closed_normally} = undef; # closed properly with QUIT
  10879. $self;
  10880. }
  10881. sub preserve_evidence # try to preserve temporary files etc in case of trouble
  10882. { my($self)=shift; !@_ ? $self->{preserve} : ($self->{preserve}=shift) }
  10883. sub DESTROY {
  10884. my($self) = shift;
  10885. eval { do_log(5,"Amavis::In::SMTP DESTROY called") };
  10886. eval {
  10887. $self->{fh_pers}->close
  10888. or die "Error closing temp file: $!" if $self->{fh_pers};
  10889. $self->{fh_pers} = undef;
  10890. my($errn) = $self->{tempdir_pers} eq '' ? ENOENT
  10891. : (stat($self->{tempdir_pers}) ? 0 : 0+$!);
  10892. if (defined $self->{tempdir_pers} && $errn != ENOENT) {
  10893. # this will not be included in the TIMING report,
  10894. # but it only occurs infrequently and doesn't take that long
  10895. if ($self->preserve_evidence && !$self->{tempdir_empty}) {
  10896. do_log(-1,"SMTP shutdown: tempdir is to be PRESERVED: ".
  10897. $self->{tempdir_pers});
  10898. } else {
  10899. do_log(3, sprintf("SMTP shutdown: %s is being removed: %s%s",
  10900. $self->{tempdir_empty} ? 'empty tempdir' : 'tempdir',
  10901. $self->{tempdir_pers},
  10902. $self->preserve_evidence ? ', nothing to preserve' : ''));
  10903. rmdir_recursively($self->{tempdir_pers});
  10904. }
  10905. }
  10906. if (ref($self->{sock}) && ! $self->{session_closed_normally}) {
  10907. $self->smtp_resp(1,"421 4.3.2 Service shutting down, closing channel");
  10908. }
  10909. };
  10910. if ($@ ne '')
  10911. { my($eval_stat) = $@; eval { do_log(1,"SMTP shutdown: $eval_stat") } }
  10912. }
  10913. sub prepare_tempdir($) {
  10914. my($self) = @_;
  10915. if (! defined $self->{tempdir_pers} ) {
  10916. # invent a name for a temporary directory for this child, and create it
  10917. my($now_iso8601) = iso8601_timestamp(time,1); # or: iso8601_utc_timestamp
  10918. $self->{tempdir_pers} = sprintf("%s/amavis-%s-%05d",
  10919. $TEMPBASE, $now_iso8601, $$);
  10920. }
  10921. my($dname) = $self->{tempdir_pers};
  10922. my(@stat_list) = lstat($dname); my($errn) = @stat_list ? 0 : 0+$!;
  10923. if (!$errn && ! -d _) { # exists, but is not a directory !?
  10924. die "prepare_tempdir: $dname is not a directory!!!";
  10925. } elsif (!$errn) {
  10926. my($dev,$ino) = @stat_list;
  10927. if ($dev != $self->{tempdir_dev} || $ino != $self->{tempdir_ino}) {
  10928. do_log(-1,"prepare_tempdir: $dname is no longer the same directory!!!");
  10929. ($self->{tempdir_dev},$self->{tempdir_ino}) = @stat_list;
  10930. }
  10931. } elsif ($errn == ENOENT) {
  10932. do_log(4,"prepare_tempdir: creating directory $dname");
  10933. mkdir($dname,0750) or die "Can't create directory $dname: $!";
  10934. @stat_list = lstat($dname); add_entropy(@stat_list);
  10935. ($self->{tempdir_dev},$self->{tempdir_ino}) = @stat_list;
  10936. $self->{tempdir_empty} = 1;
  10937. section_time('mkdir tempdir');
  10938. }
  10939. # prepare temporary file for writing (and reading later)
  10940. my($fname) = $dname . '/email.txt';
  10941. @stat_list = lstat($fname); $errn = @stat_list ? 0 : 0+$!;
  10942. if ($errn == ENOENT) { # no file
  10943. do_log(0,"$fname no longer exists, can't re-use it") if $self->{fh_pers};
  10944. $self->{fh_pers} = undef;
  10945. } elsif ($errn) { # some other error
  10946. die "prepare_tempdir: can't access $fname: $!";
  10947. $self->{fh_pers} = undef;
  10948. } elsif (! -f _) { # not a regular file !?
  10949. die "prepare_tempdir: $fname is not a regular file!!!";
  10950. $self->{fh_pers} = undef;
  10951. } elsif ($self->{fh_pers}) {
  10952. my($dev,$ino) = @stat_list;
  10953. if ($dev != $self->{file_dev} || $ino != $self->{file_ino}) {
  10954. # may happen if some user code has replaced the file, e.g. by altermime
  10955. do_log(1,"$fname is no longer the same file, won't re-use it, deleting");
  10956. unlink($fname) or die "Can't remove file $fname: $!";
  10957. $self->{fh_pers} = undef;
  10958. }
  10959. }
  10960. if ($self->{fh_pers}) {
  10961. $self->{fh_pers}->seek(0,0) or die "Can't rewind mail file: $!";
  10962. $self->{fh_pers}->truncate(0) or die "Can't truncate mail file: $!";
  10963. } else {
  10964. do_log(4,"prepare_tempdir: creating file $fname");
  10965. $self->{fh_pers} = IO::File->new($fname,'+>',0640)
  10966. or die "Can't create file $fname: $!";
  10967. @stat_list = lstat($fname); add_entropy(@stat_list);
  10968. ($self->{file_dev}, $self->{file_ino}) = @stat_list;
  10969. section_time('create email.txt');
  10970. }
  10971. }
  10972. sub authenticate($$$) {
  10973. my($state,$auth_mech,$auth_resp) = @_;
  10974. my($result,$newchallenge);
  10975. if ($auth_mech eq 'ANONYMOUS') { # rfc2245
  10976. $result = [$auth_resp,undef];
  10977. } elsif ($auth_mech eq 'PLAIN') { # rfc2595, "user\0authname\0pass"
  10978. if (!defined($auth_resp)) { $newchallenge = '' }
  10979. else { $result = [ (split(/\000/,$auth_resp,-1))[0,2] ] }
  10980. } elsif ($auth_mech eq 'LOGIN' && !defined $state) {
  10981. $newchallenge = 'Username:'; $state = [];
  10982. } elsif ($auth_mech eq 'LOGIN' && @$state==0) {
  10983. push(@$state, $auth_resp); $newchallenge = 'Password:';
  10984. } elsif ($auth_mech eq 'LOGIN' && @$state==1) {
  10985. push(@$state, $auth_resp); $result = $state;
  10986. } # CRAM-MD5:rfc2195, DIGEST-MD5:rfc2831
  10987. ($state,$result,$newchallenge);
  10988. }
  10989. # Accept a SMTP or LMTP connect (which can do any number of transactions)
  10990. # and call content checking for each message received
  10991. #
  10992. sub process_smtp_request($$$$) {
  10993. my($self, $sock, $lmtp, $conn, $check_mail) = @_;
  10994. # $sock: connected socket from Net::Server
  10995. # $lmtp: use LMTP protocol instead of (E)SMTP
  10996. # $conn: information about client connection
  10997. # $check_mail: subroutine ref to be called with file handle
  10998. my($msginfo,$authenticated,$auth_user,$auth_pass);
  10999. $self->{sock} = $sock;
  11000. $self->{pipelining} = 0; # may we buffer responses?
  11001. $self->{smtp_outbuf} = []; # SMTP responses buffer for PIPELINING
  11002. my($myheloname);
  11003. # $myheloname = $myhostname;
  11004. # $myheloname = 'localhost';
  11005. # $myheloname = '[127.0.0.1]';
  11006. $myheloname = '[' . $conn->socket_ip . ']';
  11007. new_am_id(undef, $Amavis::child_invocation_count, undef);
  11008. my($initial_am_id) = 1; my($sender,@recips); my($got_rcpt);
  11009. my($max_recip_size_limit); # maximum of per-recipient message size limits
  11010. my($terminating,$aborting,$eof,$voluntary_exit); my($seq) = 0;
  11011. my(%xforward_args); my(%baseline_policy_bank); my($policy_changed);
  11012. %baseline_policy_bank = %current_policy_bank; $policy_changed = 0;
  11013. $conn->smtp_proto($self->{proto} = $lmtp ? 'LMTP' : 'SMTP');
  11014. # system-wide message size limit, if any
  11015. my($message_size_limit) = c('smtpd_message_size_limit');
  11016. if ($message_size_limit && $message_size_limit < 65536)
  11017. { $message_size_limit = 65536 } # rfc2821 requires at least 64k
  11018. my($smtpd_greeting_banner_tmp) = c('smtpd_greeting_banner');
  11019. $smtpd_greeting_banner_tmp =~
  11020. s{ \$ (?: \{ ([^\}]*) \} | ([a-zA-Z0-9_-]+) ) }
  11021. { { 'helo-name' => $myheloname,
  11022. 'version' => $myversion,
  11023. 'version-id' => $myversion_id,
  11024. 'version-date' => $myversion_date,
  11025. 'product' => $myproduct_name,
  11026. 'protocol' => $lmtp?'LMTP':'ESMTP' }->{lc($1.$2)}
  11027. }egx;
  11028. $self->smtp_resp(1, "220 $smtpd_greeting_banner_tmp");
  11029. $0 = sprintf("amavisd (ch%d-idle)", $Amavis::child_invocation_count);
  11030. Amavis::Timing::go_idle(4);
  11031. local($_); local($/) = "\012"; # input line terminator set to LF
  11032. for (undef $!; defined($_=<$sock>); undef $!) {
  11033. $0 = sprintf("amavisd (ch%d-%s)",
  11034. $Amavis::child_invocation_count, am_id());
  11035. Amavis::Timing::go_busy(5);
  11036. prolong_timer('reading SMTP command');
  11037. { # a block is used as a 'switch' statement - 'last' will exit from it
  11038. my($cmd) = $_;
  11039. do_log(4, $self->{proto} . "< $cmd");
  11040. !/^ \s* ([A-Za-z]+) (?: \s+ (.*?) )? \s* \015\012 \z/xs && do {
  11041. $self->smtp_resp(1,"500 5.5.2 Error: bad syntax", 1, $cmd); last;
  11042. };
  11043. $_ = uc($1); my($args) = $2;
  11044. # (causes holdups in Postfix, it doesn't retry immediately; better set max_use)
  11045. # $Amavis::child_task_count >= $max_requests # exceeded max_requests
  11046. # && /^(?:HELO|EHLO|LHLO|DATA|NOOP)\z/ && do { # pipelining checkpoints
  11047. # # in case of multiple-transaction protocols (e.g. SMTP, LMTP)
  11048. # # we do not like to keep running indefinitely at the MTA's mercy
  11049. # my($msg) = "Closing transmission channel ".
  11050. # "after $Amavis::child_task_count transactions, $_";
  11051. # do_log(2,$msg); $self->smtp_resp(1,"421 4.3.0 ".$msg);
  11052. # $terminating=1; last;
  11053. # };
  11054. /^(?:RSET|DATA|QUIT)\z/ && $args ne '' && do {
  11055. $self->smtp_resp(1,"501 5.5.4 Error: $_ does not accept arguments",
  11056. 1,$cmd);
  11057. last;
  11058. };
  11059. /^RSET\z/ && do { $sender = undef; @recips = (); $got_rcpt = 0;
  11060. $max_recip_size_limit = undef; $msginfo = undef;
  11061. if ($policy_changed) {
  11062. %current_policy_bank = %baseline_policy_bank;
  11063. $policy_changed = 0;
  11064. }
  11065. $self->smtp_resp(0,"250 2.0.0 Ok $_"); last;
  11066. };
  11067. /^NOOP\z/ && do { $self->smtp_resp(1,"250 2.0.0 Ok $_"); last };
  11068. /^QUIT\z/ && do {
  11069. my($smtpd_quit_banner_tmp) = c('smtpd_quit_banner');
  11070. $smtpd_quit_banner_tmp =~
  11071. s{ \$ (?: \{ ([^\}]*) \} | ([a-zA-Z0-9_-]+) ) }
  11072. { { 'helo-name' => $myheloname,
  11073. 'version' => $myversion,
  11074. 'version-id' => $myversion_id,
  11075. 'version-date' => $myversion_date,
  11076. 'product' => $myproduct_name,
  11077. 'protocol' => $lmtp?'LMTP':'ESMTP' }->{lc($1.$2)}
  11078. }egx;
  11079. $self->smtp_resp(1,"221 2.0.0 $smtpd_quit_banner_tmp");
  11080. $terminating=1; last;
  11081. };
  11082. ### !$lmtp && /^HELO\z/ && do { # strict
  11083. /^HELO\z/ && do {
  11084. $sender = undef; @recips = (); $got_rcpt = 0; # implies RSET
  11085. $max_recip_size_limit = undef; $msginfo = undef; # forget previous
  11086. if ($policy_changed)
  11087. { %current_policy_bank = %baseline_policy_bank; $policy_changed = 0 }
  11088. $self->{pipelining} = 0; $self->smtp_resp(0,"250 $myheloname");
  11089. $lmtp = 0; $conn->smtp_proto($self->{proto} = 'SMTP');
  11090. $conn->smtp_helo($args); section_time('SMTP HELO'); last;
  11091. };
  11092. ### (!$lmtp && /^EHLO\z/ || $lmtp && /^LHLO\z/) && do { # strict
  11093. /^(?:EHLO|LHLO)\z/ && do {
  11094. $sender = undef; @recips = (); $got_rcpt = 0; # implies RSET
  11095. $max_recip_size_limit = undef; $msginfo = undef; # forget previous
  11096. if ($policy_changed)
  11097. { %current_policy_bank = %baseline_policy_bank; $policy_changed = 0 }
  11098. $lmtp = /^LHLO\z/ ? 1 : 0;
  11099. $conn->smtp_proto($self->{proto} = $lmtp ? 'LMTP' : 'ESMTP');
  11100. $self->{pipelining} = 1;
  11101. $self->smtp_resp(0,"250 $myheloname\n" . join("\n",
  11102. 'PIPELINING',
  11103. !defined($message_size_limit) ? 'SIZE'
  11104. : sprintf('SIZE %d',$message_size_limit),
  11105. '8BITMIME',
  11106. 'ENHANCEDSTATUSCODES',
  11107. !@{ca('auth_mech_avail')} ? ()
  11108. : join(' ','AUTH',@{ca('auth_mech_avail')}),
  11109. 'XFORWARD NAME ADDR PROTO HELO' ));
  11110. $conn->smtp_helo($args); section_time("SMTP $_");
  11111. last;
  11112. };
  11113. /^XFORWARD\z/ && do { # Postfix extension
  11114. if (defined($sender)) {
  11115. $self->smtp_resp(0,"503 5.5.1 Error: XFORWARD not allowed within transaction", 1, $cmd);
  11116. last;
  11117. }
  11118. my($bad);
  11119. for (split(' ',$args)) {
  11120. if (!/^( [A-Za-z0-9] [A-Za-z0-9-]* ) = ( [\041-\176]{0,255} )\z/xs) {
  11121. $self->smtp_resp(0,"501 5.5.4 Syntax error in XFORWARD parameters",
  11122. 1, $cmd);
  11123. $bad = 1; last;
  11124. } else {
  11125. my($name,$val) = (uc($1), $2);
  11126. if ($name =~ /^(?:NAME|ADDR|PROTO|HELO)\z/) {
  11127. $val = undef if uc($val) eq '[UNAVAILABLE]';
  11128. $xforward_args{$name} = $val;
  11129. } else {
  11130. $self->smtp_resp(0,"501 5.5.4 XFORWARD command parameter error: $name=$val",1,$cmd);
  11131. $bad = 1; last;
  11132. }
  11133. }
  11134. }
  11135. $self->smtp_resp(1,"250 2.5.0 Ok $_") if !$bad;
  11136. last;
  11137. };
  11138. /^HELP\z/ && do {
  11139. $self->smtp_resp(1,"214 2.0.0 See amavisd-new home page at:\n".
  11140. "http://www.ijs.si/software/amavisd/");
  11141. last;
  11142. };
  11143. /^AUTH\z/ && @{ca('auth_mech_avail')} && do { # rfc2554
  11144. if ($args !~ /^([^ ]+)(?: ([^ ]*))?\z/is) {
  11145. $self->smtp_resp(0,"501 5.5.2 Syntax: AUTH mech [initresp]",1,$cmd);
  11146. last;
  11147. }
  11148. my($auth_mech,$auth_resp) = (uc($1), $2);
  11149. if ($authenticated) {
  11150. $self->smtp_resp(0,"503 5.5.1 Error: session already authenticated", 1, $cmd);
  11151. } elsif (defined($sender)) {
  11152. $self->smtp_resp(0,"503 5.5.1 Error: AUTH not allowed within transaction", 1, $cmd);
  11153. } elsif (!grep {uc($_) eq $auth_mech} @{ca('auth_mech_avail')}) {
  11154. $self->smtp_resp(0,"504 5.7.6 Error: requested authentication mechanism not supported", 1, $cmd);
  11155. } else {
  11156. my($state,$result,$challenge);
  11157. if ($auth_resp eq '=') { $auth_resp = '' } # zero length
  11158. elsif ($auth_resp eq '') { $auth_resp = undef }
  11159. for (;;) {
  11160. if ($auth_resp !~ m{^[A-Za-z0-9+/=]*\z}) {
  11161. $self->smtp_resp(0,"501 5.5.4 Authentication failed: malformed authentication response", 1, $cmd);
  11162. last;
  11163. } else {
  11164. $auth_resp = decode_base64($auth_resp) if $auth_resp ne '';
  11165. ($state,$result,$challenge) =
  11166. authenticate($state, $auth_mech, $auth_resp);
  11167. if (ref($result) eq 'ARRAY') {
  11168. $self->smtp_resp(0,"235 2.7.1 Authentication successful");
  11169. $authenticated = 1; ($auth_user,$auth_pass) = @$result;
  11170. do_log(2,"AUTH $auth_mech, user=$auth_user");
  11171. # do_log(2,"AUTH $auth_mech, user=$auth_user, pass=$auth_resp");
  11172. last;
  11173. } elsif (defined $result && !$result) {
  11174. $self->smtp_resp(0,"535 5.7.1 Authentication failed", 1, $cmd);
  11175. last;
  11176. }
  11177. }
  11178. # server challenge or ready prompt
  11179. $self->smtp_resp(1,"334 ".encode_base64($challenge,''));
  11180. undef $!; $auth_resp = <$sock>;
  11181. defined $auth_resp || $!==0 or die "Error reading auth resp: $!";
  11182. do_log(5, $self->{proto} . "< $auth_resp");
  11183. $auth_resp =~ s/\015?\012\z//;
  11184. if ($auth_resp eq '*') {
  11185. $self->smtp_resp(0,"501 5.7.1 Authentication aborted");
  11186. last;
  11187. }
  11188. }
  11189. }
  11190. last;
  11191. };
  11192. /^VRFY\z/ && do {
  11193. $self->smtp_resp(1,"502 5.5.1 Command $_ not implemented", 1, $cmd);
  11194. # if ($args eq '') {
  11195. # $self->smtp_resp(1,"501 5.5.2 Syntax: VRFY address", 1, $cmd);
  11196. # } else {
  11197. # $self->smtp_resp(1,"252 2.0.0 Cannot VRFY user, but will accept ".
  11198. # "message and attempt delivery", 0, $cmd);
  11199. # }
  11200. last;
  11201. };
  11202. /^MAIL\z/ && do { # begin new SMTP transaction
  11203. if (defined($sender)) {
  11204. $self->smtp_resp(0,"503 5.5.1 Error: nested MAIL command", 1, $cmd);
  11205. last;
  11206. }
  11207. if (!$authenticated &&
  11208. c('auth_required_inp') && @{ca('auth_mech_avail')} ) {
  11209. $self->smtp_resp(0,"530 5.7.1 Authentication required", 1, $cmd);
  11210. last;
  11211. }
  11212. # begin SMTP transaction
  11213. my($now) = time;
  11214. prolong_timer('MAIL FROM received - timer reset', $child_timeout);
  11215. if (!$seq) { # the first connect
  11216. section_time('SMTP pre-MAIL');
  11217. } else { # establish new time reference for each transaction
  11218. Amavis::Timing::init(); snmp_counters_init();
  11219. }
  11220. $seq++;
  11221. new_am_id(undef,$Amavis::child_invocation_count,$seq)
  11222. if !$initial_am_id;
  11223. $initial_am_id = 0;
  11224. Amavis::check_mail_begin_task();
  11225. $self->prepare_tempdir;
  11226. my($cl_ip) = $xforward_args{'ADDR'};
  11227. if ($cl_ip ne '' && defined $policy_bank{'MYNETS'}
  11228. && lookup_ip_acl($cl_ip,@{ca('mynetworks_maps')}) ) {
  11229. Amavis::load_policy_bank('MYNETS'); $policy_changed = 1;
  11230. }
  11231. $msginfo = Amavis::In::Message->new;
  11232. $msginfo->rx_time($now);
  11233. # $msginfo->body_type('7bit'); # presumed, unless explicitly declared
  11234. $msginfo->delivery_method(c('forward_method'));
  11235. my($submitter);
  11236. if ($authenticated) {
  11237. $msginfo->auth_user($auth_user); $msginfo->auth_pass($auth_pass);
  11238. $conn->smtp_proto($self->{proto}.'A') # rfc3848
  11239. if $self->{proto} =~ /^(LMTP|ESMTP)\z/i;
  11240. } elsif (c('auth_reauthenticate_forwarded') &&
  11241. c('amavis_auth_user') ne '') {
  11242. $msginfo->auth_user(c('amavis_auth_user'));
  11243. $msginfo->auth_pass(c('amavis_auth_pass'));
  11244. $submitter = quote_rfc2821_local(c('mailfrom_notify_recip'));
  11245. }
  11246. $msginfo->client_addr($xforward_args{'ADDR'});
  11247. $msginfo->client_name($xforward_args{'NAME'});
  11248. $msginfo->client_proto($xforward_args{'PROTO'});
  11249. $msginfo->client_helo($xforward_args{'HELO'});
  11250. %xforward_args = (); # reset values for the next transaction
  11251. # permit some sloppy syntax without angle brackets
  11252. if ($args !~ /^FROM: \s*
  11253. ( < (?: " (?: \\. | [^\\"] )* " | [^"@] )*
  11254. (?: @ (?: \[ (?: \\. | [^\]\\] )* \] |
  11255. [^\[\]\\>] )* )?
  11256. > |
  11257. [^<\s] (?: " (?: \\. | [^\\"] )* " | [^"\s] )*
  11258. ) (?: \s+ ([\040-\176]+) )? \z/isx ) {
  11259. $self->smtp_resp(0,"501 5.5.2 Syntax: MAIL FROM: <address>",1,$cmd);
  11260. last;
  11261. }
  11262. my($bad); my($addr,$opt) = ($1,$2);
  11263. for (split(' ',$opt)) {
  11264. if (!/^ ( [A-Za-z0-9] [A-Za-z0-9-]* ) =
  11265. ( [\041-\074\076-\176]+ ) \z/xs) { # printable, not '=' or SP
  11266. $self->smtp_resp(0,"501 5.5.4 Syntax error in MAIL FROM parameters",
  11267. 1,$cmd);
  11268. $bad = 1; last;
  11269. } else {
  11270. my($name,$val) = (uc($1),$2);
  11271. if ($name eq 'SIZE' && $val=~/^\d{1,20}\z/) { # rfc1870
  11272. $msginfo->msg_size($val+0);
  11273. if ($message_size_limit && $val > $message_size_limit) {
  11274. my($msg) = "552 5.3.4 Declared message size ($val B) ".
  11275. "exceeds fixed size limit";
  11276. do_log(0, $self->{proto}." REJECT 'MAIL FROM': $msg");
  11277. $self->smtp_resp(0,$msg, 0,$cmd);
  11278. $bad = 1; last;
  11279. }
  11280. } elsif ($name eq 'BODY' && $val=~/^(?:7BIT|8BITMIME)\z/i){
  11281. $msginfo->body_type(uc($val));
  11282. } elsif ($name eq 'AUTH' && @{ca('auth_mech_avail')} &&
  11283. !defined($submitter) ) { # rfc2554
  11284. $submitter = $val; # encoded as xtext: rfc3461
  11285. $submitter =~ s/\+([0-9a-fA-F]{2})/pack("C",hex($1))/eg;
  11286. do_log(5, "MAIL command, $authenticated, submitter: $submitter");
  11287. } else {
  11288. my($msg);
  11289. if ($name eq 'AUTH' && !@{ca('auth_mech_avail')}) {
  11290. $msg = "503 5.7.4 Error: authentication disabled";
  11291. } else {
  11292. $msg = "504 5.5.4 MAIL command parameter error: $name=$val";
  11293. }
  11294. $self->smtp_resp(0,$msg,1,$cmd);
  11295. $bad = 1; last;
  11296. }
  11297. }
  11298. }
  11299. if (!$bad) {
  11300. $addr = ($addr =~ /^<(.*)>\z/s) ? $1 : $addr;
  11301. $self->smtp_resp(0,"250 2.1.0 Sender $addr OK");
  11302. $sender = unquote_rfc2821_local($addr);
  11303. if ($sender ne '' && defined $policy_bank{'MYUSERS'}
  11304. && lookup(0,$sender,@{ca('local_domains_maps')})) {
  11305. Amavis::load_policy_bank('MYUSERS'); $policy_changed = 1;
  11306. }
  11307. debug_oneshot(lookup(0,$sender,@{ca('debug_sender_maps')}) ? 1 : 0,
  11308. $self->{proto} . "< $cmd");
  11309. # $submitter = $addr if !defined($submitter); # rfc2554: MAY
  11310. $submitter = '<>' if !defined($msginfo->auth_user);
  11311. $msginfo->auth_submitter($submitter);
  11312. };
  11313. last;
  11314. };
  11315. /^RCPT\z/ && do {
  11316. if (!defined($sender)) {
  11317. $self->smtp_resp(0,"503 5.5.1 Need MAIL command before RCPT",1,$cmd);
  11318. @recips = (); $got_rcpt = 0;
  11319. last;
  11320. }
  11321. $got_rcpt++;
  11322. # permit some sloppy syntax without angle brackets
  11323. if ($args !~ /^TO: \s*
  11324. ( < (?: " (?: \\. | [^\\"] )* " | [^"@] )*
  11325. (?: @ (?: \[ (?: \\. | [^\]\\] )* \] |
  11326. [^\[\]\\>] )* )?
  11327. > |
  11328. [^<\s] (?: " (?: \\. | [^\\"] )* " | [^"\s] )*
  11329. ) (?: \s+ ([\040-\176]+) )? \z/isx ) {
  11330. $self->smtp_resp(0,"501 5.5.2 Syntax: RCPT TO: <address>",1,$cmd);
  11331. last;
  11332. }
  11333. if ($2 ne '') {
  11334. $self->smtp_resp(0,"504 5.5.4 RCPT command parameter not implemented: $2",
  11335. 1, $cmd);
  11336. ### $self->smtp_resp(0,"555 5.5.4 RCPT command parameter unrecognized: $2", 1, $cmd);
  11337. } elsif ($got_rcpt > $smtpd_recipient_limit) {
  11338. $self->smtp_resp(0,"452 4.5.3 Too many recipients");
  11339. } else {
  11340. my($addr,$opt) = ($1, $2);
  11341. $addr = ($addr =~ /^<(.*)>\z/s) ? $1 : $addr;
  11342. my($addr_unq) = unquote_rfc2821_local($addr);
  11343. my($recip_size_limit); my($mslm) = ca('message_size_limit_maps');
  11344. $recip_size_limit = lookup(0,$addr_unq, @$mslm) if @$mslm;
  11345. if ($recip_size_limit && $recip_size_limit < 65536)
  11346. { $recip_size_limit = 65536 } # rfc2821 requires at least 64k
  11347. if ($recip_size_limit > $max_recip_size_limit)
  11348. { $max_recip_size_limit = $recip_size_limit }
  11349. my($mail_size) = $msginfo->msg_size;
  11350. if (defined $mail_size && $recip_size_limit && $mail_size > $recip_size_limit) {
  11351. my($msg) = "552 5.3.4 Declared message size ($mail_size B) ".
  11352. "exceeds recipient's size limit <$addr>";
  11353. do_log(0, $self->{proto}." REJECT 'RCPT TO': $msg");
  11354. $self->smtp_resp(0,$msg, 0,$cmd);
  11355. } else {
  11356. push(@recips,$addr_unq);
  11357. $self->smtp_resp(0,"250 2.1.5 Recipient $addr OK");
  11358. my ($user, $domain) = split('@', $addr);
  11359. if (defined $recipient_policy_bank_map{$addr}) {
  11360. Amavis::load_policy_bank($recipient_policy_bank_map{$addr});
  11361. do_log(1, sprintf("Policy bank '%s' taken for recp '%s'",
  11362. $recipient_policy_bank_map{$addr},
  11363. $addr));
  11364. } elsif (defined $recipient_policy_bank_map{$domain}) {
  11365. Amavis::load_policy_bank($recipient_policy_bank_map{$domain});
  11366. do_log(1, sprintf("Policy bank '%s' taken for recp '%s'",
  11367. $recipient_policy_bank_map{$domain},
  11368. $addr));
  11369. }
  11370. foreach my $recipient_re (keys(%recipient_policy_bank_re_map)) {
  11371. if ($addr =~ /$recipient_re/) {
  11372. Amavis::load_policy_bank($recipient_policy_bank_re_map{$recipient_re});
  11373. do_log(1, sprintf("Policy bank '%s' taken for recp '%s'",
  11374. $recipient_policy_bank_re_map{$domain},
  11375. $addr));
  11376. }
  11377. }
  11378. }
  11379. };
  11380. last;
  11381. };
  11382. /^DATA\z/ && !@recips && do {
  11383. if (!defined($sender)) {
  11384. $self->smtp_resp(1,"503 5.5.1 Need MAIL command before DATA",1,$cmd);
  11385. } elsif (!$got_rcpt) {
  11386. $self->smtp_resp(1,"503 5.5.1 Need RCPT command before DATA",1,$cmd);
  11387. } elsif ($lmtp) { # rfc2033 requires 503 code!
  11388. $self->smtp_resp(1,"503 5.1.1 Error (DATA): no valid recipients",0,$cmd);
  11389. } else {
  11390. $self->smtp_resp(1,"554 5.1.1 Error (DATA): no valid recipients",0,$cmd);
  11391. }
  11392. last;
  11393. };
  11394. /^DATA\z/ && do {
  11395. # set timer to the initial value, MTA timer starts here
  11396. prolong_timer('DATA received - timer reset', $child_timeout);
  11397. if ($message_size_limit) { # enforce system-wide size limit
  11398. if (!$max_recip_size_limit ||
  11399. $max_recip_size_limit > $message_size_limit) {
  11400. $max_recip_size_limit = $message_size_limit;
  11401. }
  11402. }
  11403. my($within_data_transfer,$complete);
  11404. my($size) = 0; my($over_size) = 0;
  11405. eval {
  11406. $msginfo->sender($sender); $msginfo->recips(\@recips);
  11407. ll(1) && do_log(1, sprintf("%s:%s:%s %s: <%s> -> %s Received: %s",
  11408. $conn->smtp_proto,
  11409. $conn->socket_ip eq $inet_socket_bind ? ''
  11410. : '['.$conn->socket_ip.']',
  11411. $conn->socket_port, $self->{tempdir_pers},
  11412. $sender, join(',', qquote_rfc2821_local(@recips)),
  11413. join(' ', ($msginfo->msg_size eq '' ? ()
  11414. : 'SIZE='.$msginfo->msg_size),
  11415. ($msginfo->body_type eq '' ? ()
  11416. : 'BODY='.$msginfo->body_type),
  11417. received_line($conn,$msginfo,am_id(),0) )
  11418. ) );
  11419. $self->smtp_resp(1,"354 End data with <CR><LF>.<CR><LF>");
  11420. $within_data_transfer = 1;
  11421. section_time('SMTP pre-DATA-flush') if $self->{pipelining};
  11422. $self->{tempdir_empty} = 0;
  11423. if ($max_recip_size_limit == 0) { # no message size limit enforced
  11424. my($ln); local($/) = "\015\012"; # input line terminator CRLF
  11425. # credativ -jw
  11426. my $in_headers = 1;
  11427. my $got_received = 0;
  11428. # credativ end
  11429. for ($!=0; defined($ln=<$sock>); $!=0) { # optimized for speed
  11430. if ($ln =~ /^\./) {
  11431. if ($ln eq ".\015\012")
  11432. { $complete = 1; $within_data_transfer = 0; last }
  11433. $ln =~ s/^\.(.+\015\012)\z/$1/s; # dot de-stuffing, rfc2821
  11434. }
  11435. $size += length($ln); # message size is defined in rfc1870
  11436. # credativ -jw
  11437. if (!$got_received && $in_headers && $ln =~ /^Received:/) {
  11438. my $header = $ln;
  11439. # the header might be broken up in different
  11440. # ways according to the length of the
  11441. # strings
  11442. $header =~ tr/\n/ /;
  11443. $header =~ tr/\t/ /;
  11444. $header =~ tr/\r/ /;
  11445. $header =~ s/ / /g;
  11446. $header =~ s/^([^;]+;).*/$1/;
  11447. if ($header =~ /\(Postfix\) with E?SMTP id ([A-Z0-9]+)(;| for)/) {
  11448. $msginfo->postfixid($1);
  11449. } elsif ($header =~ /\(Postfix, from userid \d+\) id ([A-Z0-9]+);/) {
  11450. $msginfo->postfixid($1);
  11451. }
  11452. $got_received = 1;
  11453. }
  11454. if (/^$/m) {
  11455. $in_headers = 0;
  11456. }
  11457. # credativ end
  11458. # remove \015\012: s/// slowest, chomp faster, substr(,0,-2) best
  11459. print {$self->{fh_pers}} substr($ln,0,-2),$eol
  11460. or die "Can't write to mail file: $!";
  11461. }
  11462. defined $ln || $!==0 or die "Connection broken during DATA: $!";
  11463. } else { # enforce size limit
  11464. do_log(5,"enforcing size limit $max_recip_size_limit during DATA");
  11465. my($ln); local($/) = "\015\012"; # input line terminator CRLF
  11466. for ($!=0; defined($ln=<$sock>); $!=0) {
  11467. # do_log(5, $self->{proto} . "< $ln");
  11468. if ($ln =~ /^\./) {
  11469. if ($ln eq ".\015\012")
  11470. { $complete = 1; $within_data_transfer = 0; last }
  11471. $ln =~ s/^\.(.+\015\012)\z/$1/s; # dot de-stuffing, rfc2821
  11472. }
  11473. $size += length($ln); # message size is defined in rfc1870
  11474. if (!$over_size) {
  11475. print {$self->{fh_pers}} substr($ln,0,-2),$eol
  11476. or die "Can't write to mail file: $!";
  11477. if ($max_recip_size_limit && $size > $max_recip_size_limit) {
  11478. do_log(1,"Message size exceeded $max_recip_size_limit B, ".
  11479. "skiping further input");
  11480. print {$self->{fh_pers}} $eol,"***TRUNCATED***",$eol
  11481. or die "Can't write to mail file: $!";
  11482. $over_size = 1;
  11483. }
  11484. }
  11485. }
  11486. defined $ln || $!==0 or die "Connection broken during DATA: $!";
  11487. }; # restores line terminator
  11488. $eof = 1 if !$complete;
  11489. # normal data termination, or eof on socket, or fatal error
  11490. do_log(4, $self->{proto} . "< .\015\012") if $complete;
  11491. $self->{fh_pers}->flush or die "Can't flush mail file: $!";
  11492. # On some systems you have to do a seek whenever you
  11493. # switch between reading and writing. Amongst other things,
  11494. # this may have the effect of calling stdio's clearerr(3).
  11495. # credativ -jw
  11496. my $size = $self->{fh_pers}->tell();
  11497. do_log(0, "original postfix id: ". $msginfo->postfixid . ", size: " . $size);
  11498. # XXX - nrcpts
  11499. # credativ end
  11500. $self->{fh_pers}->seek(0,1) or die "Can't seek on file: $!";
  11501. section_time('SMTP DATA');
  11502. }; # end eval
  11503. if ($@ ne '' || !$complete || $over_size) { # err or connection broken
  11504. chomp($@);
  11505. # on error, either send: '421 Shutting down',
  11506. # or: '451 Aborted, error in processing' and NOT shut down!
  11507. if ($over_size && $@ eq '' && !$within_data_transfer) {
  11508. my($msg) = "552 5.3.4 Message size ($size B) exceeds size limit";
  11509. do_log(0, $self->{proto}." REJECT: $msg");
  11510. $self->smtp_resp(0,$msg, 0,$cmd);
  11511. } elsif (!$within_data_transfer) {
  11512. my($msg) = "Error in processing: " .
  11513. !$complete && $@ eq '' ? 'incomplete' : $@;
  11514. do_log(-2, $self->{proto}." TROUBLE: 451 4.5.0 $msg");
  11515. $self->smtp_resp(1, "451 4.5.0 $msg");
  11516. ### $aborting = $msg;
  11517. } else {
  11518. $aborting = "Connection broken during data transfer" if $eof;
  11519. $aborting .= ', ' if $aborting ne '' && $@ ne '';
  11520. $aborting .= $@;
  11521. $aborting = '???' if $aborting eq '';
  11522. do_log($@ ne '' ? -1 : 3, $self->{proto}." ABORTING: ".$aborting);
  11523. }
  11524. } else { # all OK
  11525. #
  11526. # Is it acceptable to do all this processing here,
  11527. # before returning response??? According to rfc1047
  11528. # it is not a good idea! But at the moment we do not have
  11529. # much choice, amavis has no queueing mechanism and can not
  11530. # accept responsibility for delivery.
  11531. #
  11532. # check contents before responding
  11533. # check_mail() expects open file on $self->{fh_pers},
  11534. # need not be rewound
  11535. $msginfo->mail_tempdir($self->{tempdir_pers});
  11536. $msginfo->mail_text_fn($self->{tempdir_pers} . '/email.txt');
  11537. $msginfo->mail_text($self->{fh_pers});
  11538. my($declared_size) = $msginfo->msg_size;
  11539. if (!defined($declared_size)) {
  11540. } elsif ($size > $declared_size) { # shouldn't happen with decent MTA
  11541. do_log(2,"Actual message size $size B greater than the ".
  11542. "declared $declared_size B");
  11543. } elsif ($size < $declared_size) { # not unusual, but permitted
  11544. do_log(4,"Actual message size $size B, declared $declared_size B");
  11545. }
  11546. $msginfo->msg_size($size); # store actual mail size
  11547. my($smtp_resp, $exit_code, $preserve_evidence) =
  11548. &$check_mail($conn,$msginfo,$lmtp);
  11549. alarm(0); # stop the timer
  11550. if ($preserve_evidence) { $self->preserve_evidence(1) }
  11551. if ($smtp_resp !~ /^4/ &&
  11552. grep { !$_->recip_done } @{$msginfo->per_recip_data}) {
  11553. if ($msginfo->delivery_method eq '') {
  11554. do_log(2,"not all recipients done, forward_method is empty");
  11555. } else {
  11556. die "TROUBLE: (MISCONFIG) not all recipients done, " .
  11557. "forward_method is: " . $msginfo->delivery_method;
  11558. }
  11559. }
  11560. if (!$lmtp) {
  11561. do_log(4, "sending SMTP response: \"$smtp_resp\"");
  11562. $self->smtp_resp(0, $smtp_resp);
  11563. } else {
  11564. my($bounced) = $msginfo->dsn_sent;
  11565. for my $r (@{$msginfo->per_recip_data}) {
  11566. my($resp) = $r->recip_smtp_response;
  11567. if ($bounced && $smtp_resp=~/^2/ && $resp!~/^2/) {
  11568. # as the message was already bounced by us,
  11569. # MTA must not bounce it again; failure status
  11570. # needs to be converted into success!
  11571. $resp = sprintf("250 2.5.0 Ok %s, DSN %s (%s)",
  11572. $r->recip_addr, $bounced==1 ? 'sent' : 'muted', $resp);
  11573. }
  11574. do_log(4, sprintf("sending LMTP response for <%s>: \"%s\"",
  11575. $r->recip_addr, $resp));
  11576. $self->smtp_resp(0, $resp);
  11577. }
  11578. }
  11579. };
  11580. alarm(0); do_log(5,"timer stopped after DATA end");
  11581. if ($self->preserve_evidence && !$self->{tempdir_empty}) {
  11582. # keep evidence in case of trouble
  11583. do_log(-1,"PRESERVING EVIDENCE in ".$self->{tempdir_pers});
  11584. $self->{fh_pers}->close or die "Error closing mail file: $!";
  11585. $self->{fh_pers} = undef; $self->{tempdir_pers} = undef;
  11586. $self->{tempdir_empty} = 1;
  11587. }
  11588. # cleanup, but leave directory (and file handle if possible) for reuse
  11589. if ($self->{fh_pers} && !$can_truncate) {
  11590. # truncate is not standard across all Unix variants,
  11591. # it is not Posix, but is XPG4-UNIX.
  11592. # So if we can't truncate a file and leave it open,
  11593. # we have to create it anew later, at some cost.
  11594. #
  11595. $self->{fh_pers}->close or die "Error closing mail file: $!";
  11596. $self->{fh_pers} = undef;
  11597. unlink($self->{tempdir_pers}.'/email.txt')
  11598. or die "Can't delete file ".$self->{tempdir_pers}."/email.txt: $!";
  11599. section_time('delete email.txt');
  11600. }
  11601. if (defined $self->{tempdir_pers}) { # prepare for the next one
  11602. strip_tempdir($self->{tempdir_pers}); $self->{tempdir_empty} = 1;
  11603. }
  11604. $sender = undef; @recips = (); $got_rcpt = 0; # implicit RSET
  11605. $max_recip_size_limit = undef; $msginfo = undef; # forget previous
  11606. if ($policy_changed)
  11607. { %current_policy_bank = %baseline_policy_bank; $policy_changed = 0 }
  11608. $self->preserve_evidence(0); # reset
  11609. # report elapsed times by section for each transaction
  11610. # (the time for the QUIT remains unaccounted for)
  11611. do_log(2, Amavis::Timing::report());
  11612. Amavis::Timing::init(); snmp_counters_init();
  11613. last;
  11614. }; # DATA
  11615. # catchall (EXPN, TURN, unknown):
  11616. $self->smtp_resp(1,"502 5.5.1 Error: command ($_) not implemented",1,$cmd);
  11617. # $self->smtp_resp(1,"500 5.5.2 Error: command ($_) not recognized", 1,$cmd);
  11618. }; # end of 'switch' block
  11619. if ($terminating || defined $aborting) { # exit SMTP-session loop
  11620. $voluntary_exit = 1; last;
  11621. }
  11622. # rfc2920 requires a flush whenever the local TCP input buffer is
  11623. # emptied. Since we can't check it (unless we use sysread & select),
  11624. # we should do a flush here to be in compliance.
  11625. $self->smtp_resp_flush;
  11626. $0 = sprintf("amavisd (ch%d-%s-idle)",
  11627. $Amavis::child_invocation_count, am_id());
  11628. Amavis::Timing::go_idle(6);
  11629. } # end of loop
  11630. my($errn,$errs);
  11631. if (!$voluntary_exit) {
  11632. $eof = 1;
  11633. if (!defined($_)) { $errn = 0+$!; $errs = "$!" }
  11634. }
  11635. $0 = sprintf("amavisd (ch%d)", $Amavis::child_invocation_count);
  11636. Amavis::Timing::go_busy(7);
  11637. # come here when: QUIT is received, eof or err on socket, or we need to abort
  11638. $self->smtp_resp_flush; # just in case, the session might have been disconnected
  11639. my($msg) =
  11640. defined $aborting && !$eof ? "ABORTING the session: $aborting" :
  11641. defined $aborting ? $aborting :
  11642. !$terminating ? "client broke the connection without a QUIT ($errs)" : '';
  11643. do_log($aborting?-1:3, $self->{proto}.': NOTICE: '.$msg) if $msg ne '';
  11644. if (defined $aborting && !$eof)
  11645. { $self->smtp_resp(1,"421 4.3.2 Service shutting down, ".$aborting) }
  11646. $self->{session_closed_normally} = 1;
  11647. # closes connection after child_finish_hook
  11648. }
  11649. # sends a SMTP response consisting of 3-digit code and an optional message;
  11650. # slow down evil clients by delaying response on permanent errors
  11651. sub smtp_resp($$$;$$) {
  11652. my($self, $flush,$resp, $penalize,$line) = @_;
  11653. if ($penalize) {
  11654. do_log(-1, $self->{proto} . ": $resp; PENALIZE: $line");
  11655. sleep 5;
  11656. section_time('SMTP penalty wait');
  11657. }
  11658. $resp = sanitize_str($resp,1);
  11659. local($1,$2,$3,$4);
  11660. if ($resp !~ /^ ([1-5]\d\d) (\ |-|\z)
  11661. ([245] \. \d{1,3} \. \d{1,3} (?: \ |\z) )?
  11662. (.*) \z/xs)
  11663. { die "Internal error(2): bad SMTP response code: '$resp'" }
  11664. my($resp_code,$continuation,$enhanced,$tail) = ($1,$2,$3,$4);
  11665. $enhanced = '' if !defined($enhanced); # avoids a warning
  11666. my($lead_len) = length($resp_code) + 1 + length($enhanced);
  11667. while (length($tail) > 512-2-$lead_len || $tail =~ /\n/) {
  11668. # rfc2821: The maximum total length of a reply line including the
  11669. # reply code and the <CRLF> is 512 characters. More information
  11670. # may be conveyed through multiple-line replies.
  11671. my($head) = substr($tail,0,512-2-$lead_len);
  11672. if ($head =~ /^([^\n]*\n)/) { $head = $1 }
  11673. $tail = substr($tail,length($head)); chomp($head);
  11674. push(@{$self->{smtp_outbuf}}, $resp_code.'-'.$enhanced.$head);
  11675. }
  11676. push(@{$self->{smtp_outbuf}}, $resp_code.$continuation.$enhanced.$tail);
  11677. $self->smtp_resp_flush if $flush || !$self->{pipelining} ||
  11678. @{$self->{smtp_outbuf}} > 200;
  11679. }
  11680. sub smtp_resp_flush($) {
  11681. my($self) = shift;
  11682. if (ref($self->{smtp_outbuf}) && @{$self->{smtp_outbuf}}) {
  11683. if (ll(4)) {
  11684. for my $resp (@{$self->{smtp_outbuf}})
  11685. { do_log(4, $self->{proto} . "> $resp") };
  11686. }
  11687. my($stat) =
  11688. $self->{sock}->print(map { $_."\015\012" } @{$self->{smtp_outbuf}} );
  11689. @{$self->{smtp_outbuf}} = (); # prevent printing again even if error
  11690. $stat or die "Error writing a SMTP response to the socket: $!";
  11691. }
  11692. }
  11693. 1;
  11694. __DATA__
  11695. #
  11696. package Amavis::AV;
  11697. use strict;
  11698. use re 'taint';
  11699. BEGIN {
  11700. use Exporter ();
  11701. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  11702. $VERSION = '2.043';
  11703. @ISA = qw(Exporter);
  11704. }
  11705. use POSIX qw(WIFEXITED WIFSIGNALED WIFSTOPPED
  11706. WEXITSTATUS WTERMSIG WSTOPSIG);
  11707. use Errno qw(EPIPE ENOTCONN ENOENT EACCES EAGAIN ECONNRESET);
  11708. use Socket;
  11709. use IO::Socket;
  11710. use IO::Socket::UNIX;
  11711. use subs @EXPORT_OK;
  11712. use vars @EXPORT;
  11713. BEGIN {
  11714. import Amavis::Conf qw(:platform :confvars c cr ca);
  11715. import Amavis::Util qw(ll untaint min max do_log am_id rmdir_recursively
  11716. exit_status_str run_command);
  11717. import Amavis::Timing qw(section_time);
  11718. }
  11719. use vars qw(%st_socket_created %st_sock); # keep persistent state (per-socket)
  11720. # subroutine available for calling from @av_scanners list entries;
  11721. # it has the same args and returns as run_av() below
  11722. sub ask_daemon { ask_av(\&ask_daemon_internal, @_) }
  11723. sub clamav_module_init($) {
  11724. my($av_name) = @_;
  11725. # each child should reinitialize clamav module to reload databases.
  11726. my($clamav_version) = Mail::ClamAV->VERSION;
  11727. my($dbdir) = Mail::ClamAV::retdbdir();
  11728. my($clamav_obj) = Mail::ClamAV->new($dbdir);
  11729. ref $clamav_obj
  11730. or die "$av_name: Can't load db from $dbdir: $Mail::ClamAV::Error";
  11731. $clamav_obj->buildtrie;
  11732. $clamav_obj->maxreclevel($MAXLEVELS) if $MAXLEVELS;
  11733. $clamav_obj->maxfiles($MAXFILES);
  11734. $clamav_obj->maxfilesize($MAX_EXPANSION_QUOTA || 30*1024*1024);
  11735. if ($clamav_version >= 0.12) {
  11736. $clamav_obj->maxratio($MAX_EXPANSION_FACTOR);
  11737. # $clamav_obj->archivememlim(0); # limit memory usage for bzip2 (0/1)
  11738. }
  11739. do_log(2,"$av_name init");
  11740. section_time('clamav_module_init');
  11741. ($clamav_obj,$clamav_version);
  11742. }
  11743. # to be called from sub ask_clamav
  11744. use vars qw($clamav_obj $clamav_version);
  11745. sub clamav_module_internal($@) {
  11746. my($query, $bare_fnames,$names_to_parts,$tempdir, $av_name) = @_;
  11747. if (!defined $clamav_obj) {
  11748. ($clamav_obj,$clamav_version) = clamav_module_init($av_name); # first time
  11749. } elsif ($clamav_obj->statchkdir) { # db reload needed?
  11750. do_log(2, "$av_name: reloading virus database");
  11751. ($clamav_obj,$clamav_version) = clamav_module_init($av_name);
  11752. }
  11753. my($fname) = "$tempdir/parts/$query"; # file to be checked
  11754. my($part) = $names_to_parts->{$query}; # get corresponding parts object
  11755. my($options) = 0; # bitfield of options to Mail::ClamAV::scan
  11756. my($opt_archive,$opt_mail);
  11757. if ($clamav_version < 0.12) {
  11758. $opt_archive = &Mail::ClamAV::CL_ARCHIVE;
  11759. $opt_mail = &Mail::ClamAV::CL_MAIL;
  11760. } else { # >= 0.12, reflects renamed flags in libclamav 0.80
  11761. $opt_archive = &Mail::ClamAV::CL_SCAN_ARCHIVE;
  11762. $opt_mail = &Mail::ClamAV::CL_SCAN_MAIL;
  11763. }
  11764. $options |= &Mail::ClamAV::CL_SCAN_STDOPT if $clamav_version >= 0.13;
  11765. $options |= $opt_archive; # turn on ARCHIVE
  11766. $options &= ~$opt_mail; # turn off MAIL
  11767. if (ref($part) && (lc($part->type_short) eq 'mail' ||
  11768. lc($part->type_declared) eq 'message/rfc822')) {
  11769. do_log(2, "$av_name: $query - enabling option CL_MAIL");
  11770. $options |= $opt_mail; # turn on MAIL
  11771. }
  11772. my($ret) = $clamav_obj->scan(untaint($fname), $options);
  11773. my($output,$status);
  11774. if ($ret->virus) { $status = 1; $output = "INFECTED: $ret" }
  11775. elsif ($ret->clean) { $status = 0; $output = "CLEAN" }
  11776. else { $status = 2; $output = $ret->error.", errno=".$ret->errno }
  11777. ($status,$output); # return synthesised status and a result string
  11778. }
  11779. # subroutine available for calling from @av_scanners list entries;
  11780. # it has the same args and returns as run_av() below
  11781. sub ask_clamav { ask_av(\&clamav_module_internal, @_) }
  11782. my($savi_obj);
  11783. sub sophos_savi_init {
  11784. my($av_name, $command) = @_;
  11785. my(@savi_bool_options) = qw(
  11786. GrpArchiveUnpack GrpSelfExtract GrpExecutable GrpInternet GrpMSOffice
  11787. GrpMisc !GrpDisinfect !GrpClean
  11788. EnableAutoStop FullSweep FullPdf Xml
  11789. );
  11790. $savi_obj = SAVI->new;
  11791. ref $savi_obj or die "$av_name: Can't create SAVI object, err=$savi_obj";
  11792. my($status) = $savi_obj->load_data;
  11793. !defined($status) or die "$av_name: Failed to load SAVI virus data " .
  11794. $savi_obj->error_string($status) . " ($status)";
  11795. my($version) = $savi_obj->version;
  11796. ref $version or die "$av_name: Can't get SAVI version, err=$version";
  11797. do_log(2,sprintf("$av_name init: Version %s (engine %d.%d) ".
  11798. "recognizing %d viruses", $version->string,
  11799. $version->major, $version->minor, $version->count));
  11800. my($error);
  11801. if ($MAXLEVELS) {
  11802. $error = $savi_obj->set('MaxRecursionDepth', $MAXLEVELS);
  11803. !defined $error
  11804. or die "$av_name: error setting MaxRecursionDepth: err=$error";
  11805. }
  11806. $error = $savi_obj->set('NamespaceSupport', 3); # new with Sophos 3.67
  11807. !defined $error
  11808. or do_log(-1,"$av_name: error setting NamespaceSupport: err=$error");
  11809. for (@savi_bool_options) {
  11810. my($value) = /^!/ ? 0 : 1; s/^!+//;
  11811. $error = $savi_obj->set($_, $value);
  11812. !defined $error or die "$av_name: Error setting $_: err=$error";
  11813. }
  11814. section_time('sophos_savi_init');
  11815. 1;
  11816. }
  11817. sub sophos_savi_stale {
  11818. defined $savi_obj && $savi_obj->stale;
  11819. }
  11820. sub sophos_savi_reload {
  11821. if (defined $savi_obj) {
  11822. my($status) = $savi_obj->load_data();
  11823. !defined($status) or die "Failed to load SAVI virus data " .
  11824. $savi_obj->error_string($status) . " ($status)";
  11825. my($version) = $savi_obj->version;
  11826. ref $version or die "Can't get SAVI version, err=$version";
  11827. do_log(2,sprintf("Updated SAVI data: Version %s (engine %d.%d) ".
  11828. "recognizing %d viruses", $version->string,
  11829. $version->major, $version->minor, $version->count));
  11830. }
  11831. }
  11832. # to be called from sub sophos_savi
  11833. sub sophos_savi_internal {
  11834. my($query,
  11835. $bare_fnames,$names_to_parts,$tempdir, $av_name,$command,$args) = @_;
  11836. my($fname) = "$tempdir/parts/$query"; # file to be checked
  11837. if (!c('bypass_decode_parts')) {
  11838. my($part) = $names_to_parts->{$query}; # get corresponding parts object
  11839. my($mime_option_value) = 0;
  11840. if (ref($part) && (lc($part->type_short) eq 'mail' ||
  11841. lc($part->type_declared) eq 'message/rfc822')) {
  11842. do_log(2, "$av_name: $query - enabling option Mime");
  11843. $mime_option_value = 1;
  11844. }
  11845. my($error) = $savi_obj->set('Mime', $mime_option_value);
  11846. !defined $error or die sprintf("%s: Error %s option Mime: err=%s",
  11847. $av_name, $mime_option_value ? 'setting' : 'clearing', $error);
  11848. }
  11849. my($output,$status); my($result) = $savi_obj->scan($fname);
  11850. if (!ref($result)) { # error
  11851. my($msg) = "error scanning file $fname, " .
  11852. $savi_obj->error_string($result) . " ($result) $!";
  11853. if (! grep {$result == $_} (514,527,530,538,549) ) {
  11854. $status = 2; $output = "ERROR $query: $msg";
  11855. } else { # don't panic on non-fatal (encrypted, corrupted, partial)
  11856. $status = 0; $output = "CLEAN $query: $msg";
  11857. }
  11858. do_log(5,"$av_name: $output");
  11859. } elsif ($result->infected) {
  11860. $status = 1; $output = join(", ", $result->viruses) . " FOUND";
  11861. } else {
  11862. $status = 0; $output = "CLEAN $query";
  11863. }
  11864. ($status,$output); # return synthesised status and a result string
  11865. }
  11866. # subroutine available for calling from @av_scanners list entries;
  11867. # it has the same args and returns as run_av() below
  11868. sub ask_sophos_savi {
  11869. my($bare_fnames,$names_to_parts,$tempdir, $av_name,$command,$args,
  11870. $sts_clean,$sts_infected,$how_to_get_names) = @_;
  11871. if (@_ < 3+6) { # supply default arguments for backwards compatibility
  11872. $args = ["*"]; $sts_clean = [0]; $sts_infected = [1];
  11873. $how_to_get_names = qr/^(.*) FOUND$/;
  11874. }
  11875. ask_av(\&sophos_savi_internal,
  11876. $bare_fnames,$names_to_parts,$tempdir, $av_name,$command,$args,
  11877. $sts_clean, $sts_infected, $how_to_get_names);
  11878. }
  11879. # same args and returns as run_av() below,
  11880. # but prepended by a $query, which is the string to be sent to the daemon.
  11881. # Handles both UNIX and INET domain sockets.
  11882. # More than one socket may be specified for redundancy, they will be tried
  11883. # one after the other until one succeeds.
  11884. #
  11885. sub ask_daemon_internal {
  11886. my($query, # expanded query template, often a command and a file or dir name
  11887. $bare_fnames,$names_to_parts,$tempdir, $av_name,$command,$args,
  11888. $sts_clean,$sts_infected,$how_to_get_names, # regexps
  11889. ) = @_;
  11890. my($query_template_orig,$sockets) = @$args;
  11891. my($output) = ''; my($socketname,$is_inet);
  11892. if (!ref($sockets)) { $sockets = [ $sockets ] }
  11893. my($max_retries) = 2 * @$sockets; my($retries) = 0;
  11894. $SIG{PIPE} = 'IGNORE'; # 'send' to broken pipe would throw a signal
  11895. # Sophie and Trophie can accept multiple requests per session
  11896. # and return a single line response each time
  11897. my($multisession) = $av_name =~ /^(Sophie|Trophie)/i ? 1 : 0;
  11898. for (;;) { # gracefully handle cases when av child times out or restarts
  11899. @$sockets >= 1 or die "no sockets specified!?"; # sanity
  11900. $socketname = $sockets->[0]; # try the first one in the current list
  11901. $is_inet = $socketname =~ m{^/} ? 0 : 1; # simpleminded: unix vs. inet sock
  11902. eval {
  11903. if (!$st_socket_created{$socketname}) {
  11904. ll(3) && do_log(3, "$av_name: Connecting to socket " .
  11905. join(' ',$daemon_chroot_dir,$socketname).
  11906. (!$retries ? '' : ", retry #$retries") );
  11907. if ($is_inet) { # inet socket
  11908. $st_sock{$socketname} = IO::Socket::INET->new($socketname)
  11909. or die "Can't connect to INET socket $socketname: $!\n";
  11910. $st_socket_created{$socketname} = 1;
  11911. } else { # unix socket
  11912. $st_sock{$socketname} = IO::Socket::UNIX->new(Type => SOCK_STREAM)
  11913. or die "Can't create UNIX socket: $!\n";
  11914. $st_socket_created{$socketname} = 1;
  11915. $st_sock{$socketname}->connect( pack_sockaddr_un($socketname) )
  11916. or die "Can't connect to UNIX socket $socketname: $!\n";
  11917. }
  11918. }
  11919. ll(3) && do_log(3,sprintf("$av_name: Sending %s to %s socket %s",
  11920. $query, $is_inet?"INET":"UNIX", $socketname));
  11921. # UGLY: bypass send method in IO::Socket to be able to retrieve
  11922. # status/errno directly from 'send', not from 'getpeername':
  11923. defined send($st_sock{$socketname}, $query, 0)
  11924. or die "Can't send to socket $socketname: $!\n";
  11925. my($rv); my($buff) = ''; undef $!;
  11926. while (defined($rv = $st_sock{$socketname}->recv($buff,8192,0))) {
  11927. $output .= $buff;
  11928. last if $multisession || $buff eq '';
  11929. undef $!;
  11930. }
  11931. defined $rv || $!==0 || $!==ECONNRESET
  11932. or die "Error receiving from $socketname: $!\n";
  11933. if (!$multisession) {
  11934. $st_sock{$socketname}->close
  11935. or die "Error closing socket $socketname: $!\n";
  11936. $st_sock{$socketname} = undef; $st_socket_created{$socketname} = 0;
  11937. }
  11938. $! = undef;
  11939. $output ne '' or die "Empty result from $socketname\n";
  11940. };
  11941. last if $@ eq '';
  11942. # error handling (most interesting error codes are EPIPE and ENOTCONN)
  11943. chomp($@); my($err) = "$!"; my($errn) = 0+$!;
  11944. ++$retries <= $max_retries
  11945. or die "Too many retries to talk to $socketname ($@)";
  11946. # is ECONNREFUSED for INET sockets common enough too?
  11947. if ($retries <= 1 && $errn == EPIPE) { # common, don't cause concern
  11948. do_log(2,"$av_name broken pipe (don't worry), retrying ($retries)");
  11949. } else {
  11950. do_log( ($retries>1?-1:1), "$av_name: $@, retrying ($retries)");
  11951. if ($retries % @$sockets == 0) { # every time the list is exhausted
  11952. my($dly) = min(20, 1 + 5 * ($retries/@$sockets - 1));
  11953. do_log(3,"$av_name: sleeping for $dly s");
  11954. sleep($dly); # slow down a possible runaway
  11955. }
  11956. }
  11957. if ($st_socket_created{$socketname}) {
  11958. # prepare for a retry, ignore 'close' status
  11959. $st_sock{$socketname}->close;
  11960. $st_sock{$socketname} = undef; $st_socket_created{$socketname} = 0;
  11961. }
  11962. # leave good socket as the first entry in the list
  11963. # so that it will be tried first when needed again
  11964. push(@$sockets, shift @$sockets) if @$sockets>1; # circular shift left
  11965. }
  11966. (0,$output); # return synthesised status and result string
  11967. }
  11968. # ask_av is a common subroutine available to be used by ask_daemon, ask_clamav,
  11969. # ask_sophos_savi and similar front-end routines used in @av_scanners entries.
  11970. # It traverses supplied files or directory ($bare_fnames) and calls a supplied
  11971. # subroutine for each file to be scanned, summarizing the final av scan result.
  11972. # It has the same args and returns as run_av() below, prepended by a checking
  11973. # subroutine argument.
  11974. sub ask_av {
  11975. my($code) = shift; # strip away the first argument, a subroutine ref
  11976. my($bare_fnames,$names_to_parts,$tempdir, $av_name,$command,$args,
  11977. $sts_clean,$sts_infected,$how_to_get_names) = @_;
  11978. my($query_template) = ref $args eq 'ARRAY' ? $args->[0] : $args;
  11979. do_log(5, "ask_av ($av_name): query template1: $query_template");
  11980. my($checking_each_file) = $query_template =~ /\*/;
  11981. my($scan_status,@virusname); my($output) = '';
  11982. for my $f ($checking_each_file ? @$bare_fnames : ("$tempdir/parts")) {
  11983. my($query) = $query_template;
  11984. if (!$checking_each_file) { # scanner can be given a directory name
  11985. $query =~ s[{}][$tempdir/parts]g; # replace {} with directory name
  11986. do_log(3,"Using ($av_name) on dir: $query");
  11987. } else { # must check each file individually
  11988. # replace {}/* with directory name and file, and * with current file name
  11989. $query =~ s[ ({}/)? \* ]
  11990. [ !defined($1) || $1 eq '' ? $f : "$tempdir/parts/$f" ]gesx;
  11991. do_log(3,"Using ($av_name) on file: $query");
  11992. }
  11993. my($t_status,$t_output) = &$code($query, @_);
  11994. do_log(4,"ask_av ($av_name) result: $t_output");
  11995. # braindead Perl: ""=~/x{0}/ serves as explicit default for an empty regexp
  11996. if (defined $sts_infected && (
  11997. ref($sts_infected) eq 'ARRAY' ? (grep {$_==$t_status} @$sts_infected)
  11998. : ""=~/x{0}/ && $t_output=~/$sts_infected/m)) { # is infected
  11999. # test for infected first, in case both expressions match
  12000. $scan_status = 1; # 'true' indicates virus found, no errors
  12001. my(@t_virusnames) = ref($how_to_get_names) eq 'CODE'
  12002. ? &$how_to_get_names($t_output)
  12003. : ""=~/x{0}/ && $t_output=~/$how_to_get_names/gm;
  12004. @t_virusnames = map { defined $_ ? $_ : () } @t_virusnames;
  12005. push(@virusname, @t_virusnames);
  12006. $output .= $t_output . $eol;
  12007. do_log(2,"ask_av ($av_name): $f INFECTED: ".join(", ",@t_virusnames));
  12008. } elsif (!defined($sts_clean)) { # clean, but inconclusive
  12009. # by convention: undef $sts_clean means result is inconclusive,
  12010. # file appears clean, but continue scanning with other av scanners,
  12011. # the current scanner does not want to vouch for it; useful for a
  12012. # scanner like jpeg checker which tests for one vulnerability only
  12013. do_log(3,"ask_av ($av_name): $f CLEAN, but inconclusive");
  12014. } elsif (ref($sts_clean) eq 'ARRAY'
  12015. ? (grep {$_==$t_status} @$sts_clean)
  12016. : ""=~/x{0}/ && $t_output=~/$sts_clean/m) { # is clean
  12017. $scan_status = 0 if !$scan_status; # no viruses, no errors
  12018. do_log(3,"ask_av ($av_name): $f CLEAN");
  12019. } else {
  12020. do_log(-2,"ask_av ($av_name) FAILED - unexpected result: $t_output");
  12021. last; # error, bail out
  12022. }
  12023. }
  12024. if (!@$bare_fnames) { $scan_status = 0 } # no errors, no viruses
  12025. do_log(3,"$av_name result: clean") if defined($scan_status) && !$scan_status;
  12026. ($scan_status,$output,\@virusname);
  12027. }
  12028. # Call a virus scanner and parse its output.
  12029. # Returns a triplet (or die in case of failure).
  12030. # The first element of the triplet is interpreted as follows:
  12031. # - true if virus found,
  12032. # - 0 if no viruses found,
  12033. # - undef if it did not complete its job;
  12034. # the second element is a string, the text as provided by the virus scanner;
  12035. # the third element is ref to a list of virus names found (if any).
  12036. # (it is guaranteed the list will be nonempty if virus was found)
  12037. #
  12038. sub run_av {
  12039. # first three args are prepended, not part of n-tuple
  12040. my($bare_fnames, # a ref to a list of filenames to scan (basenames)
  12041. $names_to_parts, # ref to a hash that maps base file names to parts object
  12042. $tempdir, # temporary directory
  12043. $av_name, $command, $args,
  12044. $sts_clean, # a ref to a list of status values, or a regexp
  12045. $sts_infected, # a ref to a list of status values, or a regexp
  12046. $how_to_get_names, # ref to sub, or a regexp to get list of virus names
  12047. $pre_code, $post_code, # routines to be invoked before and after av
  12048. ) = @_;
  12049. my($scan_status,$virusnames,$error_str); my($output) = '';
  12050. &$pre_code(@_) if defined $pre_code;
  12051. if (ref($command) eq 'CODE') {
  12052. do_log(3,"Using $av_name: (built-in interface)");
  12053. ($scan_status,$output,$virusnames) = &$command(@_);
  12054. } else {
  12055. local($1); my(@args) = split(' ',$args);
  12056. if (grep { m{^({}/)?\*\z} } @args) { # {}/* or *, list each file
  12057. # replace asterisks with bare file names (basenames) if alone or in {}/*
  12058. @args = map { !m{^({}/)?\*\z} ? $_
  12059. : map {$1.untaint($_)} @$bare_fnames } @args;
  12060. }
  12061. for (@args) { s[{}][$tempdir/parts]g } # replace {} with directory name
  12062. # NOTE: RAV does not like '</dev/null' in its command!
  12063. ll(3) && do_log(3, "Using ($av_name): " . join(' ',$command,@args));
  12064. my($proc_fh,$pid) = run_command(undef, "&1", $command, @args);
  12065. my($nbytes,$buff);
  12066. while (($nbytes=$proc_fh->read($buff,4096)) > 0) { $output .= $buff }
  12067. defined $nbytes or die "Error reading: $!";
  12068. my($err); $proc_fh->close or $err=$!; my($child_stat) = $?;
  12069. $error_str = exit_status_str($child_stat,$err);
  12070. my($retval) = WEXITSTATUS($child_stat);
  12071. chomp($output); my($output_trimmed) = $output;
  12072. $output_trimmed =~ s/\r\n/\n/gs;
  12073. $output_trimmed =~ s/([ \t\n\r])[ \t\n\r]{4,}/$1.../gs;
  12074. $output_trimmed = "..." . substr($output_trimmed,-800)
  12075. if length($output_trimmed) > 800;
  12076. do_log(3, "run_av: $command $error_str, $output_trimmed");
  12077. # braindead Perl: ""=~/x{0}/ serves as explicit default for an empty regexp
  12078. if (!WIFEXITED($child_stat)) {
  12079. } elsif (defined $sts_infected && (
  12080. ref($sts_infected) eq 'ARRAY'
  12081. ? (grep {$_==$retval} @$sts_infected)
  12082. : ""=~/x{0}/ && $output=~/$sts_infected/m)) { # is infected
  12083. # test for infected first, in case both expressions match
  12084. $virusnames = []; # get a list of virus names by parsing output
  12085. @$virusnames = ref($how_to_get_names) eq 'CODE'
  12086. ? &$how_to_get_names($output)
  12087. : ""=~/x{0}/ && $output=~/$how_to_get_names/gm;
  12088. @$virusnames = map { defined $_ ? $_ : () } @$virusnames;
  12089. $scan_status = 1; # 'true' indicates virus found
  12090. do_log(2,"run_av ($av_name): INFECTED: ".join(", ",@$virusnames));
  12091. } elsif (!defined($sts_clean)) { # clean, but inconclusive
  12092. # by convention: undef $sts_clean means result is inconclusive,
  12093. # file appears clean, but continue scanning with other av scanners,
  12094. # the current scanner does not want to vouch for it; useful for a
  12095. # scanner like jpeg checker which tests for one vulnerability only
  12096. do_log(3,"run_av ($av_name): clean, but inconclusive");
  12097. } elsif (ref($sts_clean) eq 'ARRAY' ? (grep {$_==$retval} @$sts_clean)
  12098. : ""=~/x{0}/ && $output=~/$sts_clean/m) { # is clean
  12099. $scan_status = 0; # 'false' (but defined) indicates no viruses
  12100. do_log(3,"run_av ($av_name): CLEAN");
  12101. } else {
  12102. $error_str = "unexpected $error_str, output=\"$output_trimmed\"";
  12103. do_log(-2,"run_av ($av_name) FAILED - ".$error_str);
  12104. }
  12105. $output = $output_trimmed if length($output) > 900;
  12106. }
  12107. &$post_code(@_) if defined $post_code;
  12108. $virusnames = [] if !defined $virusnames;
  12109. @$virusnames = (undef) if $scan_status && !@$virusnames; # nonnil
  12110. if (!defined($scan_status) && defined($error_str)) {
  12111. die "$command $error_str"; # die is more informative than return value
  12112. }
  12113. ($scan_status, $output, $virusnames);
  12114. }
  12115. sub virus_scan($$$) {
  12116. my($tempdir,$firsttime,$parts_root) = @_;
  12117. my($scan_status,$output,@virusname,@detecting_scanners);
  12118. my($anyone_done); my($anyone_tried);
  12119. my($bare_fnames_ref,$names_to_parts);
  12120. my(@errors); my($j); my($tier) = 'primary';
  12121. for my $av (@{ca('av_scanners')}, "\000", @{ca('av_scanners_backup')}) {
  12122. next if !defined $av;
  12123. if ($av eq "\000") { # 'magic' separator between lists
  12124. last if $anyone_done;
  12125. do_log(-2,"WARN: all $tier virus scanners failed, considering backups");
  12126. $tier = 'secondary'; next;
  12127. }
  12128. next if !ref $av || !defined $av->[1];
  12129. if (!defined $bare_fnames_ref) { # first time: collect file names to scan
  12130. ($bare_fnames_ref,$names_to_parts) =
  12131. files_to_scan("$tempdir/parts",$parts_root);
  12132. do_log(2, "Not calling virus scanners, ".
  12133. "no files to scan in $tempdir/parts") if !@$bare_fnames_ref;
  12134. }
  12135. $anyone_tried++; my($this_status,$this_output,$this_vn);
  12136. if (!@$bare_fnames_ref) { # no files to scan?
  12137. ($this_status,$this_output,$this_vn) = (0, '', []); # declare clean
  12138. } else { # call virus scanner
  12139. eval {
  12140. ($this_status,$this_output,$this_vn) =
  12141. run_av($bare_fnames_ref,$names_to_parts,$tempdir, @$av);
  12142. };
  12143. if ($@ ne '') {
  12144. my($err) = $@; chomp($err);
  12145. $err = "$av->[0] av-scanner FAILED: $err";
  12146. do_log(-2,$err); push(@errors,$err);
  12147. $this_status = undef;
  12148. };
  12149. }
  12150. $anyone_done++ if defined $this_status;
  12151. $j++; section_time("AV-scan-$j");
  12152. if ($this_status) { # virus detected by this scanner
  12153. push(@detecting_scanners, $av->[0]);
  12154. if (!@virusname) { # store results of the first scanner detecting
  12155. @virusname = @$this_vn;
  12156. $scan_status = $this_status; $output = $this_output;
  12157. }
  12158. last if c('first_infected_stops_scan'); # stop now if we found a virus?
  12159. } elsif (!defined($scan_status)) { # tentatively keep regardless of status
  12160. $scan_status = $this_status; $output = $this_output;
  12161. }
  12162. }
  12163. if (@virusname && @detecting_scanners) {
  12164. my(@ds) = @detecting_scanners; for (@ds) { s/,/;/ } # facilitates parsing
  12165. ll(2) && do_log(2, sprintf("virus_scan: (%s), detected by %d scanners: %s",
  12166. join(', ',@virusname), scalar(@ds), join(', ',@ds)));
  12167. }
  12168. $output =~ s{\Q$tempdir\E/parts/?}{}gs if defined $output; # hide path info
  12169. if (!$anyone_tried) { die "NO VIRUS SCANNERS AVAILABLE\n" }
  12170. elsif (!$anyone_done)
  12171. { die ("ALL VIRUS SCANNERS FAILED: ".join("; ",@errors)."\n") }
  12172. ($scan_status, $output, \@virusname, \@detecting_scanners); # return a quad
  12173. }
  12174. # return a ref to a list of files to be scanned in a given directory
  12175. sub files_to_scan($$) {
  12176. my($dir,$parts_root) = @_;
  12177. my($names_to_parts) = {}; # a hash that maps base file names
  12178. # to Amavis::Unpackers::Part object
  12179. # traverse decomposed parts tree breadth-first, match it to actual files
  12180. for (my($part), my(@unvisited)=($parts_root);
  12181. @unvisited and $part=shift(@unvisited);
  12182. push(@unvisited,@{$part->children}))
  12183. { $names_to_parts->{$part->base_name} = $part if $part ne $parts_root }
  12184. my($bare_fnames_ref) = []; my(%bare_fnames);
  12185. local(*DIR); opendir(DIR,$dir) or die "Can't open directory $dir: $!";
  12186. my(@dirfiles) = readdir(DIR); # must avoid modifying dir. while traversing it
  12187. closedir(DIR) or die "Error closing directory $dir: $!";
  12188. # traverse parts directory and check for actual files
  12189. for my $f (@dirfiles) {
  12190. my($fname) = "$dir/$f";
  12191. my($errn) = lstat($fname) ? 0 : 0+$!;
  12192. next if $errn == ENOENT;
  12193. if ($errn) { die "files_to_scan: file $fname inaccessible: $!" }
  12194. if (!-r _) { # attempting to gain read access to the file
  12195. do_log(3,"files_to_scan: attempting to gain read access to $fname");
  12196. chmod(0750,untaint($fname))
  12197. or die "files_to_scan: Can't change protection on $fname: $!";
  12198. $errn = lstat($fname) ? 0 : 0+$!;
  12199. if ($errn) { die "files_to_scan: file $fname inaccessible: $!" }
  12200. if (!-r _) { die "files_to_scan: file $fname not readable" }
  12201. }
  12202. next if ($f eq '.' || $f eq '..') && -d _; # this or the parent directory
  12203. if (!-f _ || !exists $names_to_parts->{$f}) { # nonregular f. or unexpected
  12204. my($what) = -l _ ? 'symlink' : -d _ ? 'directory' : -f _ ? 'file'
  12205. : 'non-regular file';
  12206. my($msg) = "removing unexpected $what $fname";
  12207. $msg .= ", it has no corresponding parts object"
  12208. if !exists $names_to_parts->{$f};
  12209. do_log(-1, "WARN: files_to_scan: ".$msg);
  12210. if (-d _) { rmdir_recursively(untaint($fname)) }
  12211. else { unlink(untaint($fname)) or die "Can't delete $what $fname: $!" }
  12212. } elsif (-z _) {
  12213. # empty file
  12214. } else {
  12215. if ($f !~ /^[A-Za-z0-9_.-]+\z/s)
  12216. {do_log(-1,"WARN: files_to_scan: unexpected/suspicious file name: $f")}
  12217. push(@$bare_fnames_ref,$f); $bare_fnames{$f} = 1;
  12218. }
  12219. }
  12220. # remove entries from %$names_to_parts that have no corresponding files
  12221. my($fname,$part);
  12222. while ( ($fname,$part) = each %$names_to_parts ) {
  12223. next if exists $bare_fnames{$fname};
  12224. if (ll(4) && $part->exists) {
  12225. my($type_short) = $part->type_short;
  12226. do_log(4,sprintf("files_to_scan: info: part %s (%s) no longer present",
  12227. $fname, (!ref $type_short ? $type_short : join(', ',@$type_short)) ));
  12228. }
  12229. delete $names_to_parts->{$fname}; # delete is allowed for the current elem.
  12230. }
  12231. ($bare_fnames_ref, $names_to_parts);
  12232. }
  12233. 1;
  12234. __DATA__
  12235. #
  12236. package Amavis::SpamControl;
  12237. use strict;
  12238. use re 'taint';
  12239. BEGIN {
  12240. use Exporter ();
  12241. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  12242. $VERSION = '2.043';
  12243. @ISA = qw(Exporter);
  12244. }
  12245. use Errno qw(EAGAIN);
  12246. use FileHandle;
  12247. use POSIX ();
  12248. use Mail::SpamAssassin;
  12249. BEGIN {
  12250. import Amavis::Conf qw(:platform :sa $daemon_user c cr ca);
  12251. import Amavis::Util qw(ll do_log exit_status_str run_command
  12252. prolong_timer add_entropy);
  12253. import Amavis::rfc2821_2822_Tools;
  12254. import Amavis::Timing qw(section_time);
  12255. import Amavis::Lookup qw(lookup);
  12256. }
  12257. use subs @EXPORT_OK;
  12258. use vars qw($spamassassin_obj);
  12259. # called at startup, before the main fork
  12260. sub init() {
  12261. do_log(1, "SpamControl: initializing Mail::SpamAssassin");
  12262. my($saved_umask) = umask;
  12263. $spamassassin_obj = Mail::SpamAssassin->new({
  12264. debug => $sa_debug,
  12265. save_pattern_hits => $sa_debug,
  12266. dont_copy_prefs => 1,
  12267. local_tests_only => $sa_local_tests_only,
  12268. home_dir_for_helpers => $helpers_home,
  12269. stop_at_threshold => 0,
  12270. site_rules_filename => $sa_site_rules_filename,
  12271. # DEF_RULES_DIR => '/usr/local/share/spamassassin',
  12272. # LOCAL_RULES_DIR => '/etc/mail/spamassassin',
  12273. #see man Mail::SpamAssassin for other options
  12274. });
  12275. # $Mail::SpamAssassin::DEBUG->{rbl}=-3;
  12276. # $Mail::SpamAssassin::DEBUG->{dcc}=-3;
  12277. # $Mail::SpamAssassin::DEBUG->{pyzor}=-3;
  12278. # $Mail::SpamAssassin::DEBUG->{bayes}=-3;
  12279. # $Mail::SpamAssassin::DEBUG->{rulesrun}=4+64;
  12280. my($sa_version) = Mail::SpamAssassin::Version();
  12281. if ($sa_auto_whitelist && $sa_version=~/^(\d+(?:\.\d+)?)/ && $1 < 3) {
  12282. do_log(1, "SpamControl: turning on SA auto-whitelisting (AWL)");
  12283. # create a factory for the persistent address list
  12284. my($addrlstfactory) = Mail::SpamAssassin::DBBasedAddrList->new;
  12285. $spamassassin_obj->set_persistent_address_list_factory($addrlstfactory);
  12286. }
  12287. $spamassassin_obj->compile_now; # try to ensure modules are preloaded
  12288. alarm(0); # seems like SA forgets to clear alarm in some cases
  12289. umask($saved_umask); # restore our umask, SA clobbered it
  12290. do_log(1, "SpamControl: done");
  12291. }
  12292. # check envelope sender if white or blacklisted by each recipient;
  12293. # Saves the result in recip_blacklisted_sender and recip_whitelisted_sender
  12294. # properties of each recipient object.
  12295. #
  12296. sub white_black_list($$$$$) {
  12297. my($conn,$msginfo,$sql_wblist,$user_id_sql,$ldap_policy) = @_;
  12298. my($any_w)=0; my($any_b)=0; my($all)=1; my($wr,$br);
  12299. my($sender) = $msginfo->sender;
  12300. do_log(4,"wbl: checking sender <$sender>");
  12301. for my $r (@{$msginfo->per_recip_data}) {
  12302. next if $r->recip_done; # already dealt with
  12303. my($found,$wb,$boost); my($recip) = $r->recip_addr;
  12304. my($user_id_ref,$mk_ref) = !defined $sql_wblist ? ([],[])
  12305. : lookup(1,$recip,$user_id_sql);
  12306. do_log(5,"wbl: (SQL) recip <$recip>, ".scalar(@$user_id_ref)." matches")
  12307. if defined $sql_wblist && ll(5);
  12308. for my $ind (0..$#{$user_id_ref}) { # for ALL SQL sets matching the recip
  12309. my($user_id) = $user_id_ref->[$ind]; my($mkey);
  12310. ($wb,$mkey) = lookup(0,$sender,
  12311. Amavis::Lookup::SQLfield->new($sql_wblist,'wb','S',$user_id) );
  12312. do_log(4,"wbl: (SQL) recip <$recip>, rid=$user_id, got: \"$wb\"");
  12313. if (!defined($wb)) { # NULL field or no match: remains undefined
  12314. } elsif ($wb =~ /^ *([+-]?\d+(?:\.\d*)?) *\z/) { # numeric
  12315. my($val) = 0+$1; # penalty points to be added to the score
  12316. $boost += $val;
  12317. ll(2) && do_log(2,sprintf(
  12318. "wbl: (SQL) soft-%slisted (%s) sender <%s> => <%s> (rid=%s)",
  12319. ($val<0?'white':'black'), $val, $sender, $recip, $user_id));
  12320. $wb = undef; # not hard- white or blacklisting
  12321. } elsif ($wb =~ /^[ \000]*\z/) { # neutral, stops the search
  12322. $found++; $wb = 0;
  12323. do_log(5,"wbl: (SQL) recip <$recip> is neutral to sender <$sender>");
  12324. } elsif ($wb =~ /^([BbNnFf])[ ]*\z/) { # blacklisted (B, N, F)
  12325. $found++; $wb = -1; $any_b++; $br = $recip;
  12326. $r->recip_blacklisted_sender(1);
  12327. do_log(5,"wbl: (SQL) recip <$recip> blacklisted sender <$sender>");
  12328. } else { # whitelisted (W, Y, T) or anything else
  12329. if ($wb =~ /^([WwYyTt])[ ]*\z/) {
  12330. do_log(5, "wbl: (SQL) recip <$recip> whitelisted sender <$sender>");
  12331. } else {
  12332. do_log(-1,"wbl: (SQL) recip <$recip> whitelisted sender <$sender>, ".
  12333. "unexpected wb field value: \"$wb\"");
  12334. }
  12335. $found++; $wb = +1; $any_w++; $wr = $recip;
  12336. $r->recip_whitelisted_sender(1);
  12337. }
  12338. last if $found;
  12339. }
  12340. if (!$found && defined($ldap_policy)) {
  12341. my($wblist);
  12342. my($keys_ref,$rhs_ref) = make_query_keys($sender,0,0);
  12343. my(@keys) = @$keys_ref;
  12344. unshift(@keys, '<>') if $sender eq ''; # a hack for a null return path
  12345. $_ = Amavis::Util::untaint($_) for @keys; # untaint keys
  12346. $_ = Net::LDAP::Util::escape_filter_value($_) for @keys;
  12347. do_log(5,sprintf("wbl: (LDAP) query keys: %s",
  12348. join(', ',map{"\"$_\""}@keys)));
  12349. $wblist = lookup(0,$recip,Amavis::Lookup::LDAPattr->new(
  12350. $ldap_policy,'amavisBlacklistSender','L-'));
  12351. for my $key (@keys) {
  12352. if (grep {/^\Q$key\E\z/i} @$wblist) {
  12353. $found++; $wb = -1; $br = $recip; $any_b++;
  12354. $r->recip_blacklisted_sender(1);
  12355. do_log(5,"wbl: (LDAP) recip <$recip> blacklisted sender <$sender>");
  12356. }
  12357. }
  12358. $wblist = lookup(0,$recip,Amavis::Lookup::LDAPattr->new(
  12359. $ldap_policy,'amavisWhitelistSender','L-'));
  12360. for my $key (@keys) {
  12361. if (grep {/^\Q$key\E\z/i} @$wblist) {
  12362. $found++; $wb = +1; $wr = $recip; $any_w++;
  12363. $r->recip_whitelisted_sender(1);
  12364. do_log(5,"wbl: (LDAP) recip <$recip> whitelisted sender <$sender>");
  12365. }
  12366. }
  12367. }
  12368. if (!$found) { # fall back to static lookups if no match
  12369. # sender can be both white- and blacklisted at the same time
  12370. my($val); my($r_ref,$mk_ref,@t);
  12371. # NOTE on the specifics of $per_recip_blacklist_sender_lookup_tables :
  12372. # the $r_ref below is supposed to be a ref to a single lookup table
  12373. # for compatibility with pre-2.0 versions of amavisd-new;
  12374. # Note that this is different from @score_sender_maps, which is
  12375. # supposed to contain a ref to a _list_ of lookup tables as a result
  12376. # of the first-level lookup (on the recipient address as a key).
  12377. #
  12378. ($r_ref,$mk_ref) = lookup(0,$recip,
  12379. Amavis::Lookup::Label->new("blacklist_recip<$recip>"),
  12380. cr('per_recip_blacklist_sender_lookup_tables'));
  12381. @t = ( (defined $r_ref ? $r_ref : ()), @{ca('blacklist_sender_maps')} );
  12382. $val = lookup(0,$sender,
  12383. Amavis::Lookup::Label->new("blacklist_sender<$sender>"),
  12384. @t) if @t;
  12385. if ($val) {
  12386. $found++; $wb = -1; $br = $recip; $any_b++;
  12387. $r->recip_blacklisted_sender(1);
  12388. do_log(5,"wbl: recip <$recip> blacklisted sender <$sender>");
  12389. }
  12390. # similar for whitelists:
  12391. ($r_ref,$mk_ref) = lookup(0,$recip,
  12392. Amavis::Lookup::Label->new("whitelist_recip<$recip>"),
  12393. cr('per_recip_whitelist_sender_lookup_tables'));
  12394. @t = ( (defined $r_ref ? $r_ref : ()), @{ca('whitelist_sender_maps')} );
  12395. $val = lookup(0,$sender,
  12396. Amavis::Lookup::Label->new("whitelist_sender<$sender>"),
  12397. @t) if @t;
  12398. if ($val) {
  12399. $found++; $wb = +1; $wr = $recip; $any_w++;
  12400. $r->recip_whitelisted_sender(1);
  12401. do_log(5,"wbl: recip <$recip> whitelisted sender <$sender>");
  12402. }
  12403. }
  12404. if (!defined($boost)) { # static lookups if no match
  12405. # note the first argument of lookup() is true, requesting ALL matches
  12406. my($r_ref,$mk_ref) = lookup(1,$recip,
  12407. Amavis::Lookup::Label->new("score_recip<$recip>"),
  12408. @{ca('score_sender_maps')});
  12409. for my $j (0..$#{$r_ref}) { # for ALL tables matching the recipient
  12410. my($val,$key) = lookup(0,$sender,
  12411. Amavis::Lookup::Label->new("score_sender<$sender>"),
  12412. @{$r_ref->[$j]} );
  12413. if (defined $val && $val != 0) {
  12414. $boost += $val;
  12415. ll(2) && do_log(2,
  12416. sprintf("wbl: soft-%slisted (%s) sender <%s> => <%s>, ".
  12417. "recip_key=\"%s\"", ($val<0?'white':'black'),
  12418. $val, $sender, $recip, $mk_ref->[$j]));
  12419. }
  12420. }
  12421. }
  12422. $r->recip_score_boost($boost) if defined $boost;
  12423. $all = 0 if !$wb;
  12424. }
  12425. if (!ll(2)) {
  12426. # don't bother preparing log report which will not be printed
  12427. } else {
  12428. my($msg) = '';
  12429. if ($all && $any_w && !$any_b) { $msg = "whitelisted" }
  12430. elsif ($all && $any_b && !$any_w) { $msg = "blacklisted" }
  12431. elsif ($all) { $msg = "black or whitelisted by all recips" }
  12432. elsif ($any_b || $any_w) {
  12433. $msg .= "whitelisted by ".($any_w>1?"$any_w recips, ":"$wr, ") if $any_w;
  12434. $msg .= "blacklisted by ".($any_b>1?"$any_b recips, ":"$br, ") if $any_b;
  12435. $msg .= "but not by all,";
  12436. }
  12437. do_log(2,"wbl: $msg sender <$sender>") if $msg ne '';
  12438. }
  12439. ($any_w+$any_b, $all);
  12440. }
  12441. # - returns true if spam detected,
  12442. # - returns 0 if no spam found,
  12443. # - throws exception (die) in case of errors,
  12444. # or just returns undef if it did not complete its jobs
  12445. #
  12446. sub spam_scan($$) {
  12447. my($conn,$msginfo) = @_;
  12448. my($spam_level,$spam_status,$spam_report,$autolearn_status); my(@lines);
  12449. my($hdr_edits) = $msginfo->header_edits;
  12450. if (!$hdr_edits) {
  12451. $hdr_edits = Amavis::Out::EditHeader->new;
  12452. $msginfo->header_edits($hdr_edits);
  12453. }
  12454. my($dspam_signature,$dspam_result,$dspam_fname);
  12455. push(@lines, sprintf("Return-Path: %s\n", # fake a local delivery agent
  12456. qquote_rfc2821_local($msginfo->sender)));
  12457. push(@lines, sprintf("X-Envelope-To: %s\n",
  12458. join(",\n ",qquote_rfc2821_local(@{$msginfo->recips}))));
  12459. my($fh) = $msginfo->mail_text;
  12460. my($mbsl) = c('sa_mail_body_size_limit');
  12461. if ( defined $mbsl &&
  12462. ($msginfo->orig_body_size > $mbsl ||
  12463. $msginfo->msg_size > 5*1024 + $mbsl)
  12464. ) {
  12465. do_log(1,"spam_scan: not wasting time on SA, message ".
  12466. "longer than $mbsl bytes: ".
  12467. $msginfo->orig_header_size .'+'. $msginfo->orig_body_size);
  12468. } else {
  12469. if (!defined($dspam) || $dspam eq '') {
  12470. do_log(5,"spam_scan: DSPAM not available, skipping it");
  12471. } else {
  12472. # pass the mail to DSPAM, extract its result headers and feed them to SA
  12473. $dspam_fname = $msginfo->mail_tempdir . '/dspam.msg';
  12474. my($dspam_fh) = IO::File->new; # will receive output from DSPAM
  12475. $dspam_fh->open($dspam_fname, O_CREAT|O_EXCL|O_WRONLY, 0640)
  12476. or die "Can't create file $dspam_fname: $!";
  12477. $fh->seek(0,0) or die "Can't rewind mail file: $!";
  12478. my($proc_fh,$pid) = run_command('&'.fileno($fh), "&1", $dspam,
  12479. qw(--stdout --deliver=spam,innocent
  12480. --mode=tum --feature=chained,noise
  12481. --enable-signature-headers
  12482. --user), $daemon_user,
  12483. ); # --mode=teft
  12484. # qw(--stdout --deliver-spam) # dspam < 3.0
  12485. # keep X-DSPAM-*, ignore other changes e.g. Content-Transfer-Encoding
  12486. my($all_local) = !grep { !lookup(0,$_,@{ca('local_domains_maps')}) }
  12487. @{$msginfo->recips};
  12488. my($first_line); my($ln);
  12489. # scan mail header from DSPAM
  12490. for (undef $!; defined($ln=$proc_fh->getline); undef $!) {
  12491. $dspam_fh->print($ln) or die "Can't write to $dspam_fname: $!";
  12492. if (!defined($first_line))
  12493. { $first_line = $ln; do_log(5,"spam_scan: from DSPAM: $first_line") }
  12494. last if $ln eq $eol;
  12495. local($1,$2);
  12496. if ($ln =~ /^(X-DSPAM[^:]*):[ \t]*(.*)$/) { # does not handle folding
  12497. my($hh,$hb) = ($1,$2);
  12498. $dspam_signature = $hb if $ln =~ /^X-DSPAM-Signature:/i;
  12499. $dspam_result = $hb if $ln =~ /^X-DSPAM-Result:/i;
  12500. do_log(3,$ln); push(@lines,$ln); # store header in array passed to SA
  12501. # add DSPAM header fields to passed mail for all recipients
  12502. $hdr_edits->append_header($hh,$hb) if $all_local;
  12503. }
  12504. }
  12505. defined $ln || $!==0 || $!==EAGAIN
  12506. or die "Error reading from DSPAM process: $!";
  12507. my($nbytes,$buff);
  12508. while (($nbytes=$proc_fh->read($buff,16384)) > 0) { #copy body from DSPAM
  12509. $dspam_fh->print($buff) or die "Can't write to $dspam_fname: $!";
  12510. }
  12511. defined $nbytes or die "Error reading: $!";
  12512. my($err); $proc_fh->close or $err = $!; my($retval) = $?;
  12513. $dspam_fh->close or die "Error closing $dspam_fname: $!";
  12514. $retval==0 && $err==0 && defined $first_line
  12515. or do_log(-1,sprintf("WARN: DSPAM problem, %s, result=%s",
  12516. exit_status_str($retval,$err), $first_line) );
  12517. do_log(4,"spam_scan: DSPAM gave: $dspam_signature, $dspam_result");
  12518. section_time('DSPAM');
  12519. }
  12520. # read mail into memory (horror!) in preparation for SpamAssasin
  12521. $fh->seek(0,0) or die "Can't rewind mail file: $!";
  12522. my($body_lines)=0; my($ln);
  12523. for (undef $!; defined($ln=<$fh>); undef $!) # header
  12524. { push(@lines,$ln); last if $ln eq $eol }
  12525. defined $ln || $!==0 or die "Error reading mail header: $!";
  12526. for (undef $!; defined($ln=<$fh>); undef $!) # body
  12527. { push(@lines,$ln); $body_lines++ }
  12528. defined $ln || $!==0 or die "Error reading mail body: $!";
  12529. section_time('SA msg read');
  12530. my($sa_required, $sa_tests);
  12531. my($saved_umask) = umask; my($saved_pid) = $$;
  12532. my($remaining_time) = alarm(0); # check how much time is left
  12533. eval {
  12534. # NOTE ON TIMEOUTS: SpamAssassin may use timer for its own purpose,
  12535. # disabling it before returning. It seems it only uses timer when
  12536. # external tests are enabled, so in order for our timeout to be
  12537. # useful, $sa_local_tests_only needs to be true (e.g. 1).
  12538. local $SIG{ALRM} = sub {
  12539. my($s) = Carp::longmess("SA TIMED OUT, backtrace:");
  12540. # crop at some rather arbitrary limit
  12541. if (length($s) > 900) { $s = substr($s,0,900-3) . "..." }
  12542. do_log(-1,$s);
  12543. };
  12544. # prepared to wait no more than n seconds
  12545. alarm($sa_timeout) if $sa_timeout > 0;
  12546. my($mail_obj); my($sa_version) = Mail::SpamAssassin::Version();
  12547. do_log(5,"calling SA parse, SA version $sa_version");
  12548. #first save our spamassassin config
  12549. my %conf_backup = ();
  12550. $spamassassin_obj->copy_config(undef, \%conf_backup) ||
  12551. die "config: error returned from copy_config!\n";
  12552. do_log(4,"SA Config saved");
  12553. # *** note that $sa_version could be 3.0.1, which is not really numeric!
  12554. if ($sa_version=~/^(\d+(?:\.\d+)?)/ && $1 >= 3) {
  12555. my($pbname) = c('policy_bank_name');
  12556. if ($pbname ne '') {
  12557. my ($rule_name) = c('sa_site_rules_filename');
  12558. $pbname =~ s/^pb_//;
  12559. if ($rule_name) {
  12560. $spamassassin_obj->read_scoreonly_config ($rule_name);
  12561. } else {
  12562. $spamassassin_obj->read_scoreonly_config ('/etc/spamassassin/multiconf/10_' . $pbname . ".cf");
  12563. }
  12564. }
  12565. $mail_obj = $spamassassin_obj->parse(\@lines);
  12566. } else { # 2.63 or earlier
  12567. $mail_obj = Mail::SpamAssassin::NoMailAudit->new(data => \@lines,
  12568. add_From_line => 0);
  12569. }
  12570. section_time('SA parse');
  12571. do_log(4,"CALLING SA check");
  12572. my($per_msg_status);
  12573. { local($1,$2,$3,$4,$5,$6); # avoid Perl 5.8.0 bug, $1 gets tainted
  12574. $per_msg_status = $spamassassin_obj->check($mail_obj);
  12575. }
  12576. my($rem_t) = alarm(0);
  12577. do_log(4,"RETURNED FROM SA check, time left: $rem_t s");
  12578. { local($1,$2,$3,$4); # avoid Perl 5.8.0..5.8.3...? taint bug
  12579. $spam_level = $per_msg_status->get_hits;
  12580. $sa_required = $per_msg_status->get_required_hits; # not used
  12581. if ($sa_version=~/^(\d+(?:\.\d+)?)/ && $1 >= 3) {
  12582. # access private SA method, unsupported
  12583. $sa_tests = $per_msg_status->_get_tag('TESTSSCORES',',');
  12584. $autolearn_status = $per_msg_status->get_autolearn_status;
  12585. } else {
  12586. $sa_tests = $per_msg_status->get_names_of_tests_hit;
  12587. }
  12588. $spam_report = $per_msg_status->get_report; # taints $1 and $2 !
  12589. # example of how to gather aditional information from SA:
  12590. # my($trusted) = $per_msg_status->_get_tag('RELAYSTRUSTED');
  12591. # $hdr_edits->append_header('X-TESTING',$trusted);
  12592. #Experimental, unfinished:
  12593. # $per_msg_status->rewrite_mail;
  12594. # my($entity) = nomailaudit_to_mime_entity($mail_obj);
  12595. $per_msg_status->finish;
  12596. #now copy our config back
  12597. $spamassassin_obj->copy_config(\%conf_backup, undef) ||
  12598. die "config: error returned from copy_config!\n";
  12599. do_log(4,"SA Config restored");
  12600. }
  12601. };
  12602. section_time('SA check');
  12603. umask($saved_umask); # SA changes umask to 0077
  12604. if ($$ != $saved_pid) {
  12605. eval { do_log(-2,"PANIC, SA produced a clone process ".
  12606. "of [$saved_pid], TERMINATING CLONE [$$]") };
  12607. POSIX::_exit(1); # avoid END and destructor processing
  12608. }
  12609. prolong_timer('spam_scan_SA', $remaining_time); # restart the timer
  12610. if ($@ ne '') { # SA timed out?
  12611. chomp($@);
  12612. die "$@\n" if $@ ne "timed out";
  12613. }
  12614. $sa_tests =~ s/,\s*/,/g; $spam_status = "tests=[" . $sa_tests . "]";
  12615. add_entropy($spam_level,$sa_tests);
  12616. if (defined $dspam && $dspam ne '' && defined $spam_level) { # auto-learn
  12617. my($eat,@options);
  12618. @options = (qw(--stdout --mode=tum --user), $daemon_user); # --mode=teft
  12619. if ( $spam_level > 7.0 && $dspam_result eq 'Innocent') {
  12620. $eat = 'SPAM'; push(@options, qw(--class=spam --source=error));
  12621. }
  12622. elsif ($spam_level < 0.5 && $dspam_result eq 'Spam') {
  12623. $eat = 'HAM'; push(@options, qw(--class=innocent --source=error));
  12624. }
  12625. if (defined $eat && $dspam_signature ne '') {
  12626. do_log(2,"DSPAM learn $eat ($spam_level), $dspam_signature");
  12627. my($proc_fh,$pid) = run_command($dspam_fname, "&1", $dspam, @options);
  12628. # consume remaining output to avoid broken pipe
  12629. my($nbytes,$buff);
  12630. while (($nbytes=$proc_fh->read($buff,4096)) > 0) { }
  12631. defined $nbytes or die "Error reading from DSPAM process: $!";
  12632. my($err); $proc_fh->close or $err = $!; my($retval) = $?;
  12633. # do_log(-1,"DSPAM learn $eat response:".$output) if $output ne '';
  12634. $retval==0 && $err==0
  12635. or die ("DSPAM learn $eat FAILED: ".exit_status_str($retval,$err));
  12636. section_time('DSPAM learn');
  12637. }
  12638. }
  12639. }
  12640. if (defined $dspam_fname) {
  12641. if (($spam_level > 5.0 ? 1 : 0) != ($dspam_result eq 'Spam' ? 1 : 0))
  12642. { do_log(2,"DSPAM: different opinions: $dspam_result, $spam_level") }
  12643. unlink($dspam_fname) or die "Can't delete file $dspam_fname: $!";
  12644. }
  12645. do_log(3,"spam_scan: score=$spam_level $spam_status");
  12646. ($spam_level, $spam_status, $spam_report, $autolearn_status);
  12647. }
  12648. #sub nomailaudit_to_mime_entity($) {
  12649. # my($mail_obj) = @_; # expect a Mail::SpamAssassin::MsgContainer object
  12650. # my(@m_hdr) = $mail_obj->header; # in array context returns array of lines
  12651. # my($m_body) = $mail_obj->body; # returns array ref
  12652. # my($entity);
  12653. # # make sure _our_ source line number is reported in case of failure
  12654. # eval {$entity = MIME::Entity->build(
  12655. # Type => 'text/plain', Encoding => '-SUGGEST',
  12656. # Data => $m_body); 1} or do {chomp($@); die $@};
  12657. # my($head) = $entity->head;
  12658. # # insert header fields from template into MIME::Head entity
  12659. # for my $hdr_line (@m_hdr) {
  12660. # # make sure _our_ source line number is reported in case of failure
  12661. # eval {$head->replace($fhead,$fbody); 1} or do {chomp($@); die $@};
  12662. # }
  12663. # $entity; # return the built MIME::Entity
  12664. #}
  12665. 1;
  12666. __DATA__
  12667. #
  12668. package Amavis::Unpackers;
  12669. use strict;
  12670. use re 'taint';
  12671. BEGIN {
  12672. use Exporter ();
  12673. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  12674. $VERSION = '2.043';
  12675. @ISA = qw(Exporter);
  12676. @EXPORT_OK = qw(&init &decompose_part &determine_file_types);
  12677. }
  12678. use Errno qw(ENOENT EACCES EAGAIN);
  12679. use IO::File qw(O_CREAT O_EXCL O_WRONLY);
  12680. use File::Basename qw(basename);
  12681. use Convert::TNEF;
  12682. use Convert::UUlib 1.05 qw(:constants); # avoid security bug in 1.04 and older
  12683. use Compress::Zlib 1.35; # avoid security vulnerability in <= 1.34
  12684. use Archive::Tar;
  12685. use Archive::Zip 1.14 qw(:CONSTANTS :ERROR_CODES);
  12686. BEGIN {
  12687. import Amavis::Util qw(untaint min max ll do_log retcode exit_status_str
  12688. snmp_count prolong_timer sanitize_str run_command
  12689. rmdir_recursively add_entropy);
  12690. import Amavis::Conf qw(:platform :confvars $file c cr ca);
  12691. import Amavis::Timing qw(section_time);
  12692. import Amavis::Lookup qw(lookup);
  12693. import Amavis::Unpackers::MIME qw(mime_decode);
  12694. import Amavis::Unpackers::NewFilename qw(consumed_bytes);
  12695. }
  12696. use subs @EXPORT_OK;
  12697. # recursively descend into a directory $dir containing potentially unsafe
  12698. # files with unpredictable names, soft links, etc., rename each regular
  12699. # nonempty file to directory $outdir giving it a generated name,
  12700. # and discard all the rest, including the directory $dir.
  12701. # Return a pair: number of bytes that 'sanitized' files now occupy,
  12702. # and a number of parts objects created.
  12703. #
  12704. sub flatten_and_tidy_dir($$$;$$); # prototype
  12705. sub flatten_and_tidy_dir($$$;$$) {
  12706. my($dir, $outdir, $parent_obj, $item_num_offset, $orig_names) = @_;
  12707. do_log(4, "flatten_and_tidy_dir: processing directory \"$dir\"");
  12708. my($cnt_r,$cnt_u) = (0,0); my($consumed_bytes) = 0;
  12709. my($item_num) = 0; my($parent_placement) = $parent_obj->mime_placement;
  12710. chmod(0750, $dir) or die "Can't change protection of \"$dir\": $!";
  12711. local(*DIR); opendir(DIR,$dir) or die "Can't open directory \"$dir\": $!";
  12712. my(@dirfiles) = readdir(DIR); # must avoid modifying dir. while traversing it
  12713. closedir(DIR) or die "Error closing directory \"$dir\": $!";
  12714. for my $f (@dirfiles) {
  12715. my($msg); my($fname) = "$dir/$f";
  12716. my(@stat_list) = lstat($fname); my($errn) = @stat_list ? 0 : 0+$!;
  12717. if ($errn == ENOENT) { $msg = "does not exist" }
  12718. elsif ($errn) { $msg = "inaccessible: $!" }
  12719. if (defined $msg) { die "flatten_and_tidy_dir: \"$fname\" $msg," }
  12720. next if ($f eq '.' || $f eq '..') && -d _;
  12721. add_entropy(@stat_list);
  12722. my($newpart_obj) = Amavis::Unpackers::Part->new($outdir,$parent_obj);
  12723. $item_num++;
  12724. $newpart_obj->mime_placement(sprintf("%s/%d",$parent_placement,
  12725. $item_num+$item_num_offset) );
  12726. # save tainted original member name if available, or a tainted file name
  12727. my($original_name) = !ref($orig_names) ? undef : $orig_names->{$f};
  12728. $newpart_obj->name_declared(defined $original_name ? $original_name : $f);
  12729. # untaint, but if $dir happens to still be tainted, we want to know and die
  12730. $fname = $dir.'/'.untaint($f);
  12731. if (-d _) {
  12732. $newpart_obj->attributes_add('D');
  12733. my($bytes,$cnt) = flatten_and_tidy_dir($fname, $outdir, $parent_obj,
  12734. $item_num+$item_num_offset, $orig_names);
  12735. $consumed_bytes += $bytes; $item_num += $cnt;
  12736. } elsif (-l _) {
  12737. $cnt_u++; $newpart_obj->attributes_add('L');
  12738. unlink($fname) or die "Can't remove soft link \"$fname\": $!";
  12739. } elsif (!-f _) {
  12740. do_log(4, "flatten_and_tidy_dir: NONREGULAR FILE \"$fname\"");
  12741. $cnt_u++; $newpart_obj->attributes_add('S');
  12742. unlink($fname) or die "Can't remove nonregular file \"$fname\": $!";
  12743. } elsif (-z _) {
  12744. $cnt_u++;
  12745. unlink($fname) or die "Can't remove empty file \"$fname\": $!";
  12746. } else {
  12747. chmod(0750, $fname)
  12748. or die "Can't change protection of file \"$fname\": $!";
  12749. my($size) = 0 + (-s _);
  12750. $newpart_obj->size($size);
  12751. $consumed_bytes += $size;
  12752. my($newpart) = $newpart_obj->full_name;
  12753. ll(5) && do_log(5,
  12754. sprintf("flatten_and_tidy_dir: renaming \"%s\"%s to %s", $fname,
  12755. !defined $original_name ? '' : " ($original_name)", $newpart));
  12756. $cnt_r++;
  12757. rename($fname, $newpart)
  12758. or die "Can't rename \"$fname\" to $newpart: $!";
  12759. }
  12760. }
  12761. rmdir($dir) or die "Can't remove directory \"$dir\": $!";
  12762. section_time("ren$cnt_r-unl$cnt_u-files$item_num");
  12763. ($consumed_bytes, $item_num);
  12764. }
  12765. # call 'file(1)' utility for each part,
  12766. # and associate (save) full and short types with each part
  12767. #
  12768. sub determine_file_types($$) {
  12769. my($tempdir, $partslist_ref) = @_;
  12770. $file ne '' or die "Unix utility file(1) not available, but is needed";
  12771. my($cwd) = "$tempdir/parts";
  12772. my(@part_list) = grep { $_->exists } @$partslist_ref;
  12773. if (!@part_list) { do_log(5, "no parts, file(1) not called") }
  12774. else {
  12775. local($1,$2); # avoid Perl taint bug (5.8.3), $cwd and $arg are not tainted
  12776. # but $arg becomes tainted because $1 is tainted from before
  12777. my(@file_list) = # collect full file names, remove cwd if possible
  12778. map { my($n) = $_->full_name; $n =~ s{^\Q$cwd\E/(.*)\z}{$1}s; $n }
  12779. @part_list;
  12780. chdir($cwd) or die "Can't chdir to $cwd: $!";
  12781. my($proc_fh,$pid) = run_command(undef, "&1", $file, @file_list);
  12782. chdir($TEMPBASE) or die "Can't chdir to $TEMPBASE: $!";
  12783. my($index)=0; my($ln);
  12784. for (undef $!; defined($ln=$proc_fh->getline); undef $!) {
  12785. chomp($ln);
  12786. do_log(5, "result line from file(1): $ln");
  12787. if ($index > $#file_list) {
  12788. do_log(-1, "NOTICE: Skipping extra output from file(1): $ln");
  12789. } else {
  12790. my($part) = $part_list[$index]; # walk through @part_list in sync
  12791. my($expect) = $file_list[$index]; # walk through @file_list in sync
  12792. if ($ln !~ /^(\Q$expect\E):[ \t]*(.*)\z/s) { #split file name from type
  12793. do_log(-1,"NOTICE: Skipping bad output from file(1) ".
  12794. "at [$index, $expect], got: $ln");
  12795. } else {
  12796. my($type_short); my($actual_name) = $1; my($type_long) = $2;
  12797. $type_short = lookup(0,$type_long,@map_full_type_to_short_type_maps);
  12798. ll(4) && do_log(4, sprintf("File-type of %s: %s%s",
  12799. $part->base_name, $type_long,
  12800. (!defined $type_short ? ''
  12801. : !ref $type_short ? "; ($type_short)"
  12802. : '; (' . join(', ',@$type_short) . ')'
  12803. ) ));
  12804. $part->type_long($type_long); $part->type_short($type_short);
  12805. $part->attributes_add('C') # simpleminded
  12806. if !ref($type_short) ? $type_short eq 'pgp' # encrypted?
  12807. : grep {$_ eq 'pgp'} @$type_short;
  12808. $index++;
  12809. }
  12810. }
  12811. }
  12812. defined $ln || $!==0 || $!==EAGAIN
  12813. or die "Error reading from file(1) utility: $!";
  12814. if ($index < @part_list) {
  12815. die sprintf("parsing file(1) results - missing last %d results",
  12816. @part_list - $index);
  12817. }
  12818. my($err); $proc_fh->close or $err = $!;
  12819. $?==0 or die ("'file' utility ($file) failed, ".exit_status_str($?,$err));
  12820. section_time(sprintf('get-file-type%d', scalar(@part_list)));
  12821. }
  12822. }
  12823. sub decompose_mail($$) {
  12824. my($tempdir,$file_generator_object) = @_;
  12825. my($hold); my(@parts); my($depth) = 1; my($any_undecipherable) = 0;
  12826. my($which_section) = "parts_decode";
  12827. # fetch all not-yet-visited part names, and start a new cycle
  12828. TIER:
  12829. while (@parts = @{$file_generator_object->parts_list}) {
  12830. if ($MAXLEVELS && $depth > $MAXLEVELS) {
  12831. $hold = "Maximum decoding depth ($MAXLEVELS) exceeded";
  12832. last;
  12833. }
  12834. $file_generator_object->parts_list_reset; # new names cycle
  12835. # clip to avoid very long log entries
  12836. my(@chopped_parts) = @parts > 5 ? @parts[0..4] : @parts;
  12837. ll(4) && do_log(4,sprintf("decode_parts: level=%d, #parts=%d : %s",
  12838. $depth, scalar(@parts),
  12839. join(', ', (map { $_->base_name } @chopped_parts),
  12840. (@chopped_parts >= @parts ? () : "...")) ));
  12841. for my $part (@parts) { # test for existence of all expected files
  12842. my($fname) = $part->full_name;
  12843. my($errn) = $fname eq '' ? ENOENT : lstat($fname) ? 0 : 0+$!;
  12844. if ($errn == ENOENT) {
  12845. $part->exists(0);
  12846. # $part->type_short('no-file') if !defined $part->type_short;
  12847. } elsif ($errn) {
  12848. die "decompose_mail: inaccessible file $fname: $!";
  12849. } elsif (!-f _) { # not a regular file
  12850. my($what) = -l _ ? 'symlink' : -d _ ? 'directory' : 'non-regular file';
  12851. do_log(-1, "WARN: decompose_mail: removing unexpected $what $fname");
  12852. if (-d _) { rmdir_recursively($fname) }
  12853. else { unlink($fname) or die "Can't delete $what $fname: $!" }
  12854. $part->exists(0);
  12855. $part->type_short(-l _ ? 'symlink' : -d _ ? 'dir' : 'special')
  12856. if !defined $part->type_short;
  12857. } elsif (-z _) { # empty file
  12858. unlink($fname) or die "Can't remove \"$fname\": $!";
  12859. $part->exists(0);
  12860. $part->type_short('empty') if !defined $part->type_short;
  12861. $part->type_long('empty') if !defined $part->type_long;
  12862. } else {
  12863. $part->exists(1);
  12864. }
  12865. }
  12866. determine_file_types($tempdir, \@parts);
  12867. for my $part (@parts) {
  12868. if ($part->exists && !defined($hold))
  12869. { $hold = decompose_part($part, $tempdir) }
  12870. $any_undecipherable++ if grep {$_ eq 'U'} @{ $part->attributes || [] };
  12871. }
  12872. last TIER if defined $hold;
  12873. $depth++;
  12874. }
  12875. section_time($which_section); prolong_timer($which_section);
  12876. ($hold, $any_undecipherable);
  12877. }
  12878. # Decompose the part
  12879. sub decompose_part($$) {
  12880. my($part, $tempdir) = @_;
  12881. # possible return values from eval:
  12882. # 0 - truly atomic, or unknown or archiver failure; consider atomic
  12883. # 1 - some archive, successfully unpacked, result replaces original
  12884. # 2 - probably unpacked, but keep the original (eg self-extracting archive)
  12885. my($hold,$none_called);
  12886. my($sts) = eval {
  12887. my($type_short) = $part->type_short;
  12888. my(@ts) = !defined $type_short ? ()
  12889. : !ref $type_short ? ($type_short) : @$type_short;
  12890. return 0 if !@ts; # consider atomic if unknown (returns from eval)
  12891. snmp_count("OpsDecType-".join('.',@ts));
  12892. for my $dec_tuple (@{ca('decoders')}) { # first matching decoder wins
  12893. next if !defined $dec_tuple;
  12894. my($dec_ts,$code,@args) = @$dec_tuple;
  12895. if ($code && grep {$_ eq $dec_ts} @ts)
  12896. { return &$code($part,$tempdir,@args) } # returns from eval
  12897. }
  12898. # falling through (e.g. HTML) - no match, consider atomic
  12899. $none_called = 1;
  12900. return 0; # returns from eval
  12901. };
  12902. if ($@ ne '') {
  12903. chomp($@);
  12904. if ($@ =~ /^Exceeded storage quota/ ||
  12905. $@ =~ /^Maximum number of files\b.*\bexceeded/) { $hold = $@ }
  12906. else {
  12907. do_log(-1,sprintf("Decoding of %s (%s) failed, leaving it unpacked: %s",
  12908. $part->base_name, $part->type_long, $@));
  12909. }
  12910. $sts = 2;
  12911. chdir($TEMPBASE) or die "Can't chdir to $TEMPBASE: $!"; # just in case
  12912. }
  12913. if ($sts == 1 && lookup(0,$part->type_long, @keep_decoded_original_maps)) {
  12914. # don't trust this file type or unpacker,
  12915. # keep both the original and the unpacked file
  12916. ll(4) && do_log(4,sprintf("file type is %s, retain original %s",
  12917. $part->type_long, $part->base_name));
  12918. $sts = 2;
  12919. }
  12920. if ($sts == 1) {
  12921. ll(5) && do_log(5, "decompose_part: deleting ".$part->full_name);
  12922. unlink($part->full_name)
  12923. or die sprintf("Can't unlink %s: %s", $part->full_name, $!);
  12924. }
  12925. ll(4) && do_log(4,sprintf("decompose_part: %s - %s", $part->base_name,
  12926. ['atomic','archive, unpacked','source retained']->[$sts]));
  12927. section_time('decompose_part') unless $none_called;
  12928. $hold;
  12929. }
  12930. # a trivial wrapper around mime_decode() to adjust arguments and result
  12931. sub do_mime_decode($$) {
  12932. my($part, $tempdir) = @_;
  12933. mime_decode($part,$tempdir,$part);
  12934. 2; # probably unpacked, but keep the original mail
  12935. };
  12936. #
  12937. # Uncompression/unarchiving routines
  12938. # Possible return codes:
  12939. # 0 - truly atomic, or unknown or archiver failure; consider atomic
  12940. # 1 - some archiver format, successfully unpacked, result replaces original
  12941. # 2 - probably unpacked, but keep the original (eg self-extracting archive)
  12942. # if ASCII text, try multiple decoding methods as provided by UUlib
  12943. # (uuencoded, xxencoded, BinHex, yEnc, Base64, Quoted-Printable)
  12944. sub do_ascii($$) {
  12945. my($part, $tempdir) = @_;
  12946. ll(4) && do_log(4,"do_ascii: Decoding part ".$part->base_name);
  12947. snmp_count('OpsDecByUUlibAttempt');
  12948. # prevent uunconc.c/UUDecode() from trying to create temp file in '/'
  12949. my($old_env_tmpdir) = $ENV{TMPDIR}; $ENV{TMPDIR} = "$tempdir/parts";
  12950. my($any_errors,$any_decoded);
  12951. eval { # must not go away without calling Convert::UUlib::CleanUp!
  12952. my($sts,$count);
  12953. $sts = Convert::UUlib::Initialize();
  12954. $sts = 0 if !defined($sts); #avoid Use of uninit. value in numeric eq (==)
  12955. $sts==RET_OK or die "Convert::UUlib::Initialize failed: ".
  12956. Convert::UUlib::strerror($sts);
  12957. my($uulib_version) = Convert::UUlib::GetOption(OPT_VERSION);
  12958. !Convert::UUlib::SetOption(OPT_IGNMODE,1) or die "bad uulib OPT_IGNMODE";
  12959. # !Convert::UUlib::SetOption(OPT_DESPERATE,1) or die "bad uulib OPT_DESPERATE";
  12960. ($sts, $count) = Convert::UUlib::LoadFile($part->full_name);
  12961. if ($sts != RET_OK) {
  12962. my($errmsg) = Convert::UUlib::strerror($sts) . ": $!";
  12963. $errmsg .= ", (???"
  12964. . Convert::UUlib::strerror(Convert::UUlib::GetOption(OPT_ERRNO))."???)"
  12965. if $sts == RET_IOERR;
  12966. die "Convert::UUlib::LoadFile (uulib V$uulib_version) failed: $errmsg";
  12967. }
  12968. ll(4) && do_log(4,sprintf(
  12969. "do_ascii: Decoding part %s (%d items), uulib V%s",
  12970. $part->base_name, $count, $uulib_version));
  12971. my($uu);
  12972. my($item_num) = 0; my($parent_placement) = $part->mime_placement;
  12973. for (my($j) = 0; $uu = Convert::UUlib::GetFileListItem($j); $j++) {
  12974. $item_num++;
  12975. ll(4) && do_log(4,sprintf(
  12976. "do_ascii(%d): state=0x%02x, enc=%s%s, est.size=%s, name=%s",
  12977. $j, $uu->state, Convert::UUlib::strencoding($uu->uudet),
  12978. ($uu->mimetype ne '' ? ", mimetype=" . $uu->mimetype : ''),
  12979. $uu->size, $uu->filename));
  12980. if (!($uu->state & FILE_OK)) {
  12981. $any_errors++;
  12982. do_log(1,"do_ascii: Convert::UUlib info: $j not decodable, ".$uu->state);
  12983. } else {
  12984. my($newpart_obj)=Amavis::Unpackers::Part->new("$tempdir/parts",$part);
  12985. $newpart_obj->mime_placement("$parent_placement/$item_num");
  12986. $newpart_obj->name_declared($uu->filename);
  12987. my($newpart) = $newpart_obj->full_name;
  12988. $! = undef;
  12989. $sts = $uu->decode($newpart); # decode to file $newpart
  12990. my($err_decode) = "$!";
  12991. chmod(0750, $newpart) or $! == ENOENT # chmod, don't panic if no file
  12992. or die "Can't change protection of \"$newpart\": $!";
  12993. my($statmsg);
  12994. my($errn) = lstat($newpart) ? 0 : 0+$!;
  12995. if ($errn == ENOENT) { $statmsg = "does not exist" }
  12996. elsif ($errn) { $statmsg = "inaccessible: $!" }
  12997. elsif ( -l _) { $statmsg = "is a symlink" }
  12998. elsif ( -d _) { $statmsg = "is a directory" }
  12999. elsif (!-f _) { $statmsg = "not a regular file" }
  13000. if (defined $statmsg) { $statmsg = "; file status: $newpart $statmsg" }
  13001. my($size) = 0 + (-s _);
  13002. $newpart_obj->size($size);
  13003. consumed_bytes($size, 'do_ascii');
  13004. if ($sts == RET_OK && $errn==0) {
  13005. $any_decoded++;
  13006. do_log(4,"do_ascii: RET_OK" . $statmsg) if defined $statmsg;
  13007. } elsif ($sts == RET_NODATA || $sts == RET_NOEND) {
  13008. $any_errors++;
  13009. do_log(-1,"do_ascii: Convert::UUlib error: "
  13010. . Convert::UUlib::strerror($sts) . $statmsg);
  13011. } else {
  13012. $any_errors++;
  13013. my($errmsg) = Convert::UUlib::strerror($sts) . ":: $err_decode";
  13014. $errmsg .= ", " . Convert::UUlib::strerror(
  13015. Convert::UUlib::GetOption(OPT_ERRNO) ) if $sts == RET_IOERR;
  13016. die ("Convert::UUlib failed: " . $errmsg . $statmsg);
  13017. }
  13018. }
  13019. }
  13020. };
  13021. my($eval_stat) = $@;
  13022. Convert::UUlib::CleanUp();
  13023. snmp_count('OpsDecByUUlib') if $any_decoded;
  13024. if (defined $old_env_tmpdir) { $ENV{TMPDIR} = $old_env_tmpdir }
  13025. else { delete $ENV{TMPDIR} }
  13026. if ($eval_stat ne '') { chomp($eval_stat); die "do_ascii: $eval_stat\n" }
  13027. ($any_decoded && !$any_errors) ? 1 : $any_errors ? 2 : 0;
  13028. }
  13029. # use Archive-Zip
  13030. sub do_unzip($$) {
  13031. my($part, $tempdir) = @_;
  13032. ll(4) && do_log(4, "Unzipping " . $part->base_name);
  13033. snmp_count('OpsDecByArZipAttempt');
  13034. my($zip) = Archive::Zip->new;
  13035. my(@err_nm) = qw(AZ_OK AZ_STREAM_END AZ_ERROR AZ_FORMAT_ERROR AZ_IO_ERROR);
  13036. # need to set up a temporary minimal error handler
  13037. Archive::Zip::setErrorHandler(sub { return 5 });
  13038. my($sts) = $zip->read($part->full_name);
  13039. Archive::Zip::setErrorHandler(sub { die @_ });
  13040. if ($sts != AZ_OK) {
  13041. do_log(4, "do_unzip: not a zip: $err_nm[$sts] ($sts)");
  13042. return 0;
  13043. }
  13044. my($any_unsupp_compmeth,$any_zero_length);
  13045. my($encryptedcount,$extractedcount) = (0,0);
  13046. my($item_num) = 0; my($parent_placement) = $part->mime_placement;
  13047. for my $mem ($zip->members()) {
  13048. my($newpart_obj) = Amavis::Unpackers::Part->new("$tempdir/parts",$part);
  13049. $item_num++; $newpart_obj->mime_placement("$parent_placement/$item_num");
  13050. $newpart_obj->name_declared($mem->fileName);
  13051. my($compmeth) = $mem->compressionMethod;
  13052. if ($compmeth != COMPRESSION_DEFLATED && $compmeth != COMPRESSION_STORED) {
  13053. $any_unsupp_compmeth = $compmeth;
  13054. $newpart_obj->attributes_add('U');
  13055. } elsif ($mem->isEncrypted) {
  13056. $encryptedcount++;
  13057. $newpart_obj->attributes_add('U','C');
  13058. } elsif ($mem->isDirectory) {
  13059. $newpart_obj->attributes_add('D');
  13060. } else {
  13061. # want to read uncompressed - set to COMPRESSION_STORED
  13062. my($oldc) = $mem->desiredCompressionMethod(COMPRESSION_STORED);
  13063. $sts = $mem->rewindData();
  13064. $sts == AZ_OK or die sprintf("%s: error rew. member data: %s (%s)",
  13065. $part->base_name, $err_nm[$sts], $sts);
  13066. my($newpart) = $newpart_obj->full_name;
  13067. my($outpart) = IO::File->new;
  13068. $outpart->open($newpart, O_CREAT|O_EXCL|O_WRONLY, 0640)
  13069. or die "Can't create file $newpart: $!";
  13070. binmode($outpart) or die "Can't set file $newpart to binmode: $!";
  13071. my($size) = 0;
  13072. while ($sts == AZ_OK) {
  13073. my($buf_ref);
  13074. ($buf_ref, $sts) = $mem->readChunk();
  13075. $sts == AZ_OK || $sts == AZ_STREAM_END
  13076. or die sprintf("%s: error reading member: %s (%s)",
  13077. $part->base_name, $err_nm[$sts], $sts);
  13078. my($buf_len) = length($$buf_ref);
  13079. if ($buf_len > 0) {
  13080. $size += $buf_len;
  13081. $outpart->print($$buf_ref) or die "Can't write to $newpart: $!";
  13082. consumed_bytes($buf_len, 'do_unzip');
  13083. }
  13084. }
  13085. $any_zero_length = 1 if $size == 0;
  13086. $newpart_obj->size($size);
  13087. $outpart->close or die "Error closing $newpart: $!";
  13088. $mem->desiredCompressionMethod($oldc);
  13089. $mem->endRead();
  13090. $extractedcount++;
  13091. }
  13092. }
  13093. snmp_count('OpsDecByArZip');
  13094. my($retval) = 1;
  13095. if ($any_unsupp_compmeth) {
  13096. $retval = 2;
  13097. do_log(-1, sprintf("do_unzip: %s, unsupported compr. method: %s",
  13098. $part->base_name, $any_unsupp_compmeth));
  13099. } elsif ($any_zero_length) { # possible zip vulnerability exploit
  13100. $retval = 2;
  13101. do_log(1, sprintf("do_unzip: %s, zero length members, archive retained",
  13102. $part->base_name));
  13103. } elsif ($encryptedcount) {
  13104. $retval = 2;
  13105. do_log(1, sprintf(
  13106. "do_unzip: %s, %d members are encrypted, %s extracted, archive retained",
  13107. $part->base_name, $encryptedcount,
  13108. !$extractedcount ? 'none' : $extractedcount));
  13109. }
  13110. $retval;
  13111. }
  13112. # use external decompressor program from the gzip/bzip2/compress family
  13113. # (there *is* a perl module for bzip2, but is not ready for prime time)
  13114. sub do_uncompress($$$) {
  13115. my($part, $tempdir, $decompressor) = @_;
  13116. ll(4) && do_log(4,sprintf("do_uncompress %s by %s",
  13117. $part->base_name,$decompressor));
  13118. my($decompressor_name) = basename((split(' ',$decompressor))[0]);
  13119. snmp_count("OpsDecBy\u${decompressor_name}");
  13120. my($newpart_obj) = Amavis::Unpackers::Part->new("$tempdir/parts",$part);
  13121. $newpart_obj->mime_placement($part->mime_placement."/1");
  13122. my($newpart) = $newpart_obj->full_name;
  13123. my($type_short, $name_declared) = ($part->type_short, $part->name_declared);
  13124. my(@rn); # collect recommended file names
  13125. push(@rn,$1)
  13126. if $part->type_long =~ /^\S+\s+compressed data, was "(.+)"(\z|, from\b)/;
  13127. for my $name_d (!ref $name_declared ? ($name_declared) : @$name_declared) {
  13128. next if $name_d eq '';
  13129. my($name) = $name_d;
  13130. for (!ref $type_short ? ($type_short) : @$type_short) {
  13131. /^F\z/ and $name=~s/\.F\z//;
  13132. /^Z\z/ and $name=~s/\.Z\z// || $name=~s/\.tg?z\z/.tar/;
  13133. /^gz\z/ and $name=~s/\.gz\z// || $name=~s/\.tgz\z/.tar/;
  13134. /^bz\z/ and $name=~s/\.bz\z// || $name=~s/\.tbz\z/.tar/;
  13135. /^bz2\z/ and $name=~s/\.bz2?\z// || $name=~s/\.tbz\z/.tar/;
  13136. /^lzo\z/ and $name=~s/\.lzo\z//;
  13137. /^rpm\z/ and $name=~s/\.rpm\z/.cpio/;
  13138. }
  13139. push(@rn,$name) if !grep { $_ eq $name } @rn;
  13140. }
  13141. $newpart_obj->name_declared(@rn==1 ? $rn[0] : \@rn) if @rn;
  13142. my($proc_fh,$pid) =
  13143. run_command($part->full_name, undef, split(' ',$decompressor));
  13144. my($rv,$rerr) = run_command_copy($newpart,$proc_fh);
  13145. if ($rv) {
  13146. # unlink($newpart) or die "Can't unlink $newpart: $!";
  13147. die sprintf('Error running decompressor %s on %s, %s',
  13148. $decompressor, $part->base_name, exit_status_str($rv,$rerr));
  13149. }
  13150. 1;
  13151. }
  13152. # use Compress::Zlib to inflate
  13153. sub do_gunzip($$) {
  13154. my($part, $tempdir) = @_; my($retval) = 0;
  13155. do_log(4, "Inflating gzip archive " . $part->base_name);
  13156. snmp_count('OpsDecByZlib');
  13157. my($gz) = Amavis::IO::Zlib->new;
  13158. $gz->open($part->full_name,'rb')
  13159. or die ("do_gunzip: Can't open gzip file ".$part->full_name.": $!");
  13160. my($newpart_obj) = Amavis::Unpackers::Part->new("$tempdir/parts",$part);
  13161. $newpart_obj->mime_placement($part->mime_placement."/1");
  13162. my($newpart) = $newpart_obj->full_name;
  13163. my($outpart) = IO::File->new;
  13164. $outpart->open($newpart, O_CREAT|O_EXCL|O_WRONLY, 0640)
  13165. or die "Can't create file $newpart: $!";
  13166. binmode($outpart) or die "Can't set file $newpart to binmode: $!";
  13167. my($nbytes,$buff); my($size) = 0;
  13168. while (($nbytes=$gz->read($buff,16384)) > 0) {
  13169. $outpart->print($buff) or die "Can't write to $newpart: $!";
  13170. $size += $nbytes; consumed_bytes($nbytes, 'do_gunzip');
  13171. }
  13172. my($err) = defined $nbytes ? 0 : $!;
  13173. $newpart_obj->size($size);
  13174. $outpart->close or die "Error closing $newpart: $!";
  13175. my(@rn); # collect recommended file name
  13176. my($name_declared) = $part->name_declared;
  13177. for my $name_d (!ref $name_declared ? ($name_declared) : @$name_declared) {
  13178. next if $name_d eq '';
  13179. my($name) = $name_d;
  13180. $name=~s/\.(gz|Z)\z// || $name=~s/\.tgz\z/.tar/;
  13181. push(@rn,$name) if !grep { $_ eq $name } @rn;
  13182. }
  13183. $newpart_obj->name_declared(@rn==1 ? $rn[0] : \@rn) if @rn;
  13184. if (defined $nbytes && $nbytes==0) { $retval = 1 } # success
  13185. else {
  13186. do_log(-1, "do_gunzip: Error reading file ".$part->full_name.": $err");
  13187. unlink($newpart) or die "Can't unlink $newpart: $!";
  13188. $newpart_obj->size(undef); $retval = 0;
  13189. }
  13190. $gz->close or die "Error closing gzipped file: $!";
  13191. $retval;
  13192. }
  13193. # untar any tar archives with Archive-Tar, extract each file individually
  13194. sub do_tar($$) {
  13195. my($part, $tempdir) = @_;
  13196. snmp_count('OpsDecByArTar');
  13197. # Work around bug in Archive-Tar
  13198. my $tar = eval { Archive::Tar->new($part->full_name) };
  13199. if (!defined($tar)) {
  13200. chomp($@);
  13201. do_log(4, sprintf("Faulty archive %s: %s", $part->full_name, $@));
  13202. return 0;
  13203. }
  13204. do_log(4,"Untarring ".$part->base_name);
  13205. my($item_num) = 0; my($parent_placement) = $part->mime_placement;
  13206. my(@list) = $tar->list_files();
  13207. for (@list) {
  13208. next if /\/\z/; # ignore directories
  13209. # this is bad (reads whole file into scalar)
  13210. # need some error handling, too
  13211. my $data = $tar->get_content($_);
  13212. my($newpart_obj) = Amavis::Unpackers::Part->new("$tempdir/parts",$part);
  13213. $item_num++; $newpart_obj->mime_placement("$parent_placement/$item_num");
  13214. my($newpart) = $newpart_obj->full_name;
  13215. my($outpart) = IO::File->new;
  13216. $outpart->open($newpart, O_CREAT|O_EXCL|O_WRONLY, 0640)
  13217. or die "Can't create file $newpart: $!";
  13218. binmode($outpart) or die "Can't set file $newpart to binmode: $!";
  13219. $outpart->print($data) or die "Can't write to $newpart: $!";
  13220. $newpart_obj->size(length($data));
  13221. consumed_bytes(length($data), 'do_tar');
  13222. $outpart->close or die "Error closing $newpart: $!";
  13223. }
  13224. 1;
  13225. }
  13226. # use external program to expand RAR archives
  13227. sub do_unrar($$$) {
  13228. my($part, $tempdir, $archiver) = @_;
  13229. ll(4) && do_log(4, "Attempting to expand RAR archive " . $part->base_name);
  13230. my($decompressor_name) = basename((split(' ',$archiver))[0]);
  13231. snmp_count("OpsDecBy\u${decompressor_name}Attempt");
  13232. my(@common_rar_switches) = qw(-c- -p- -av- -idp);
  13233. my($err, $retval, $rv1);
  13234. # unrar exit codes: SUCCESS=0, WARNING=1, FATAL_ERROR=2, CRC_ERROR=3,
  13235. # LOCK_ERROR=4, WRITE_ERROR=5, OPEN_ERROR=6, USER_ERROR=7, MEMORY_ERROR=8,
  13236. # CREATE_ERROR=9, USER_BREAK=255
  13237. # Check whether we can really unrar it
  13238. $rv1 = system($archiver, 't', '-inul', @common_rar_switches, '--',
  13239. $part->full_name);
  13240. $err = $!; $retval = retcode($rv1);
  13241. if ($retval == 7) { # USER_ERROR
  13242. do_log(-1,"do_unrar: $archiver does not recognize all switches, "
  13243. . "it is probably too old. Retrying without '-av- -idp'. "
  13244. . "Upgrade: http://www.rarlab.com/");
  13245. @common_rar_switches = qw(-c- -p-); # retry without new switches
  13246. $rv1 = system($archiver, 't', '-inul', @common_rar_switches, '--',
  13247. $part->full_name);
  13248. $err = $!; $retval = retcode($rv1);
  13249. }
  13250. if (!grep { $_ == $retval } (0,1,3)) {
  13251. # not one of: SUCCESS, WARNING, CRC_ERROR
  13252. # NOTE: password protected files in the archive cause CRC_ERROR
  13253. do_log(4,sprintf("unrar 't' %s, command: %s",
  13254. exit_status_str($rv1,$err), $archiver));
  13255. return 0;
  13256. }
  13257. # We have to jump hoops because there is no simple way to
  13258. # just list all the files
  13259. ll(4) && do_log(4, "Expanding RAR archive " . $part->base_name);
  13260. my(@list); my($hypcount) = 0; my($encryptedcount) = 0;
  13261. my($lcnt) = 0; my($member_name); my($bytes) = 0; my($last_line);
  13262. my($item_num) = 0; my($parent_placement) = $part->mime_placement;
  13263. my($proc_fh,$pid) =
  13264. run_command(undef, "&1", $archiver, 'v', @common_rar_switches, '--',
  13265. $part->full_name);
  13266. local($_);
  13267. for (undef $!; defined($_=$proc_fh->getline); undef $!) {
  13268. $last_line = $_ if !/^\s*$/; # keep last nonempty line
  13269. chomp;
  13270. if (/^unexpected end of archive/) {
  13271. last;
  13272. } elsif (/^------/) {
  13273. $hypcount++;
  13274. last if $hypcount >= 2;
  13275. } elsif ($hypcount < 1 && /^Encrypted file:/) {
  13276. do_log(4,"do_unrar: ".$_);
  13277. $part->attributes_add('U','C');
  13278. } elsif ($hypcount == 1) {
  13279. $lcnt++; local($1,$2,$3);
  13280. if ($lcnt % 2 == 0) { # information line (every other line)
  13281. if (!/^\s+(\d+)\s+(\d+)\s+(\d+%|-->|<--)/) {
  13282. do_log(-1,"do_unrar: can't parse info line for \"$member_name\" $_");
  13283. } elsif (defined $member_name) {
  13284. do_log(5,"do_unrar: member: \"$member_name\", size: $1");
  13285. if ($1 > 0) { $bytes += $1; push(@list, $member_name) }
  13286. }
  13287. $member_name = undef;
  13288. } elsif (/^(.)(.*)\z/s) {
  13289. $member_name = $2; # all but the first character (space or an asterisk)
  13290. if ($1 eq '*') { # member is encrypted
  13291. $encryptedcount++; $item_num++;
  13292. # make a phantom entry - carrying only name and attributes
  13293. my($newpart_obj) =
  13294. Amavis::Unpackers::Part->new("$tempdir/parts",$part);
  13295. $newpart_obj->mime_placement("$parent_placement/$item_num");
  13296. $newpart_obj->name_declared($member_name);
  13297. $newpart_obj->attributes_add('U','C');
  13298. $member_name = undef; # makes no sense extracting encrypted files
  13299. }
  13300. }
  13301. }
  13302. }
  13303. defined $_ || $!==0 || $!==EAGAIN or die "Error reading: $!";
  13304. # consume all remaining output to avoid broken pipe
  13305. my($ln);
  13306. for (undef $!; defined($ln=$proc_fh->getline); undef $!)
  13307. { $last_line = $ln if $ln !~ /^\s*$/ }
  13308. defined $ln || $!==0 || $!==EAGAIN or die "Error reading: $!";
  13309. $err = undef; $proc_fh->close or $err = $!; $retval = retcode($?);
  13310. if ($retval == 3) { # CRC_ERROR
  13311. do_log(4,"do_unrar: CRC_ERROR - undecipherable");
  13312. $part->attributes_add('U');
  13313. }
  13314. my($fn) = $part->full_name; local($1,$2);
  13315. if (!$bytes && $retval==0 && $last_line =~ /^\Q$fn\E is not RAR archive$/) {
  13316. do_log(4,"do_unrar: ".$last_line);
  13317. return 0;
  13318. } elsif ($last_line !~ /^\s*(\d+)\s+(\d+)/s) {
  13319. do_log(4,"do_unrar: unable to obtain orig total size: $last_line");
  13320. } else {
  13321. do_log(4,"do_unrar: summary size: $2, sum of sizes: $bytes")
  13322. if abs($bytes - $2) > 100;
  13323. $bytes = $2 if $2 > $bytes;
  13324. }
  13325. consumed_bytes($bytes, 'do_unrar-pre', 1); # pre-check on estimated size
  13326. snmp_count("OpsDecBy\u${decompressor_name}");
  13327. if ($retval==0) {} # SUCCESS
  13328. elsif ($retval==1 && @list && $bytes > 0) {} # WARNING, probably still ok
  13329. else { # WARNING and suspicious, or really bad
  13330. die ("unrar: can't get a list of archive members: " .
  13331. exit_status_str($?,$err) ."; ".$last_line);
  13332. }
  13333. if (!@list) {
  13334. do_log(4,"do_unrar: no archive members, or not an archive at all");
  13335. #***return 0 if $exec;
  13336. } else {
  13337. # my $rv = store_mgr($tempdir, $part, \@list, $archiver,
  13338. # qw(p -inul -kb), @common_rar_switches, '--',
  13339. # $part->full_name);
  13340. # unrar/rar can make the dir by itself, but can't hurt (sparc64 problem?)
  13341. mkdir("$tempdir/parts/rar", 0750)
  13342. or die "Can't mkdir $tempdir/parts/rar: $!";
  13343. my($proc_fh,$pid) =
  13344. run_command(undef, "&1", $archiver, qw(x -inul -ver -o- -kb),
  13345. @common_rar_switches, '--',
  13346. $part->full_name, "$tempdir/parts/rar/");
  13347. my($nbytes,$buff); my($output) = '';
  13348. while (($nbytes=$proc_fh->read($buff,4096)) > 0) { $output .= $buff }
  13349. defined $nbytes or die "Error reading: $!";
  13350. my($err); $proc_fh->close or $err = $!; my($retval) = retcode($?);
  13351. if (!grep { $_ == $retval } (0,1,3)) { # not one of: SUCCESS, WARNING, CRC
  13352. do_log(-1, 'unrar '.exit_status_str($?,$err));
  13353. }
  13354. my($errn) = lstat("$tempdir/parts/rar") ? 0 : 0+$!;
  13355. if ($errn != ENOENT) {
  13356. my($b) = flatten_and_tidy_dir("$tempdir/parts/rar","$tempdir/parts",$part);
  13357. consumed_bytes($b, 'do_unrar');
  13358. }
  13359. }
  13360. if ($encryptedcount) {
  13361. do_log(1, sprintf(
  13362. "do_unrar: %s, %d members are encrypted, %s extracted, archive retained",
  13363. $part->base_name, $encryptedcount, !@list ? 'none' : 0+@list ));
  13364. return 2;
  13365. }
  13366. 1;
  13367. }
  13368. # use external program to expand LHA archives
  13369. sub do_lha($$$) {
  13370. my($part, $tempdir, $archiver) = @_;
  13371. ll(4) && do_log(4, "Attempting to expand LHA archive " . $part->base_name);
  13372. my($decompressor_name) = basename((split(' ',$archiver))[0]);
  13373. snmp_count("OpsDecBy\u${decompressor_name}Attempt");
  13374. # lha needs extension .exe to understand SFX!
  13375. symlink($part->full_name, $part->full_name.".exe")
  13376. or die sprintf("Can't symlink %s %s.exe: %s",
  13377. $part->full_name, $part->full_name, $!);
  13378. # Check whether we can really lha it
  13379. my($checkerr); my($retval) = 1; my($ln);
  13380. my($proc_fh,$pid) =
  13381. run_command(undef, "&1", $archiver, 'lq', $part->full_name.".exe");
  13382. for (undef $!; defined($ln=$proc_fh->getline); undef $!)
  13383. { $checkerr = 1 if /Checksum error/i }
  13384. defined $ln || $!==0 || $!==EAGAIN or die "Error reading: $!";
  13385. my($err); $proc_fh->close or $err = $!;
  13386. if ($? || $checkerr) {
  13387. $retval = 0; # consider atomic
  13388. do_log(4, "do_lha: not a LHA archive($checkerr) ? ".
  13389. exit_status_str($?,$err));
  13390. } else {
  13391. do_log(4, "Expanding LHA archive " . $part->base_name . ".exe");
  13392. ($proc_fh,$pid) =
  13393. run_command(undef, undef, $archiver, 'lq', $part->full_name.".exe");
  13394. my(@list); my($ln);
  13395. for (undef $!; defined($ln=$proc_fh->getline); undef $!) {
  13396. chomp($ln); local($1);
  13397. next if $ln =~ m{/\z}; # ignore directories
  13398. if ($ln =~ /^(?:\S+\s+){6}\S+\s*(\S.*?)\s*\z/s) { push(@list,$1) }
  13399. else { do_log(5,"do_lha: skip: $ln") }
  13400. }
  13401. defined $ln || $!==0 || $!==EAGAIN or die "Error reading: $!";
  13402. $err=undef; $proc_fh->close or $err = $!;
  13403. $?==0 or do_log(-1, 'do_lha: '.exit_status_str($?,$err));
  13404. if (!@list) {
  13405. do_log(4, "do_lha: no archive members, or not an archive at all");
  13406. #*** $retval = 0 if $exec;
  13407. } else {
  13408. snmp_count("OpsDecBy\u${decompressor_name}");
  13409. my $rv = store_mgr($tempdir, $part, \@list, $archiver, 'pq',
  13410. $part->full_name.".exe");
  13411. do_log(-1, 'do_lha '.exit_status_str($rv)) if $rv;
  13412. $retval = 1; # consider decoded
  13413. }
  13414. }
  13415. unlink($part->full_name.".exe")
  13416. or die "Can't unlink " . $part->full_name . ".exe: $!";
  13417. $retval;
  13418. }
  13419. # use external program to expand ARC archives;
  13420. # works with original arc, or a GPL licensed 'nomarch'
  13421. # (http://rus.members.beeb.net/nomarch.html)
  13422. sub do_arc($$$) {
  13423. my($part, $tempdir, $archiver) = @_;
  13424. my($decompressor_name) = basename((split(' ',$archiver))[0]);
  13425. snmp_count("OpsDecBy\u${decompressor_name}");
  13426. my($is_nomarch) = $archiver =~ /nomarch/i;
  13427. ll(4) && do_log(4,sprintf("Unarcing %s, using %s",
  13428. $part->base_name, ($is_nomarch ? "nomarch" : "arc") ));
  13429. my($cmdargs) = ($is_nomarch ? "-l -U" : "ln") . " " . $part->full_name;
  13430. my($proc_fh,$pid) =
  13431. run_command(undef, '/dev/null', $archiver, split(' ',$cmdargs));
  13432. my(@list); my($ln);
  13433. for (undef $!; defined($ln=$proc_fh->getline); undef $!) { push(@list,$ln) }
  13434. defined $ln || $!==0 || $!==EAGAIN or die "Error reading: $!";
  13435. my($err) = 0; $proc_fh->close or $err = $!;
  13436. $err==0 && $?==0 or do_log(-1, 'do_arc: '.exit_status_str($?,$err));
  13437. #*** no spaces in filenames allowed???
  13438. map { s/^([^ \t\r\n]*).*\z/$1/s } @list; # keep only filenames
  13439. if (@list) {
  13440. my $rv = store_mgr($tempdir, $part, \@list, $archiver,
  13441. ($is_nomarch ? ('-p', '-U') : 'p'), $part->full_name);
  13442. do_log(-1, 'arc '.exit_status_str($rv)) if $rv;
  13443. }
  13444. 1;
  13445. }
  13446. # use external program to expand ZOO archives
  13447. sub do_zoo($$$) {
  13448. my($part, $tempdir, $archiver) = @_;
  13449. do_log(4, "Expanding ZOO archive " . $part->full_name);
  13450. my($decompressor_name) = basename((split(' ',$archiver))[0]);
  13451. snmp_count("OpsDecBy\u${decompressor_name}");
  13452. # Zoo needs extension of .zoo!
  13453. symlink($part->full_name, $part->full_name.".zoo")
  13454. or die sprintf("Can't symlink %s %s.zoo: %s",
  13455. $part->full_name, $part->full_name, $!);
  13456. my($proc_fh,$pid) =
  13457. run_command(undef, undef, $archiver, 'lf1q', $part->full_name.".zoo");
  13458. my(@list); my($ln);
  13459. for (undef $!; defined($ln=$proc_fh->getline); undef $!) { push(@list,$ln) }
  13460. defined $ln || $!==0 || $!==EAGAIN or die "Error reading: $!";
  13461. my($err); $proc_fh->close or $err = $!;
  13462. $?==0 or do_log(-1, 'do_zoo: '.exit_status_str($?,$err));
  13463. if (@list) {
  13464. chomp(@list);
  13465. my $rv = store_mgr($tempdir, $part, \@list, $archiver, 'xpqqq:',
  13466. $part->full_name . ".zoo");
  13467. do_log(-1, 'zoo '.exit_status_str($rv)) if $rv;
  13468. }
  13469. unlink($part->full_name.".zoo")
  13470. or die "Can't unlink " . $part->full_name . ".zoo: $!";
  13471. 1;
  13472. }
  13473. # use external program to expand ARJ archives
  13474. sub do_unarj($$$) {
  13475. my($part, $tempdir, $archiver) = @_;
  13476. do_log(4, "Expanding ARJ archive " . $part->base_name);
  13477. my($decompressor_name) = basename((split(' ',$archiver))[0]);
  13478. snmp_count("OpsDecBy\u${decompressor_name}");
  13479. # options to arj, ignored by unarj
  13480. # provide some password in -g to turn fatal error into 'bad password' error
  13481. $ENV{ARJ_SW} = "-i -jo -b5 -2h -jyc -ja1 -gsecret -w$tempdir/parts";
  13482. # unarj needs extension of .arj!
  13483. symlink($part->full_name, $part->full_name.".arj")
  13484. or die sprintf("Can't symlink %s %s.arj: %s",
  13485. $part->full_name, $part->full_name, $!);
  13486. # obtain total original size of archive members from the index/listing
  13487. my($proc_fh,$pid) =
  13488. run_command(undef,'/dev/null', $archiver, 'l', $part->full_name.".arj");
  13489. my($last_line); my($ln);
  13490. for (undef $!; defined($ln=$proc_fh->getline); undef $!)
  13491. { $last_line = $ln if $ln !~ /^\s*$/ }
  13492. defined $ln || $!==0 || $!==EAGAIN or die "Error reading: $!";
  13493. my($err); $proc_fh->close or $err = $!; my($retval) = retcode($?);
  13494. if (!grep { $_ == $retval } (0,1,3)) { # not one of: success, warn, CRC err
  13495. die ("unarj: can't get a list of archive members: ".
  13496. exit_status_str($?,$err));
  13497. }
  13498. if ($last_line !~ /^\s*(\d+)\s*files\s*(\d+)/s) {
  13499. do_log(-1,"do_unarj: WARN: unable to obtain orig size of files: $last_line");
  13500. } else {
  13501. consumed_bytes($2, 'do_unarj-pre', 1); # pre-check on estimated size
  13502. }
  13503. # unarj has very limited extraction options, arj is much better!
  13504. mkdir("$tempdir/parts/arj", 0750) or die "Can't mkdir $tempdir/parts/arj: $!";
  13505. chdir("$tempdir/parts/arj") or die "Can't chdir to $tempdir/parts/arj: $!";
  13506. ($proc_fh,$pid) =
  13507. run_command(undef, "&1", $archiver, 'e', $part->full_name.".arj");
  13508. my($encryptedcount,$skippedcount) = (0,0);
  13509. for (undef $!; defined($ln=$proc_fh->getline); undef $!) {
  13510. $encryptedcount++
  13511. if $ln =~ /^(Extracting.*\bBad file data or bad password|File is password encrypted, Skipped)\b/s;
  13512. $skippedcount++
  13513. if $ln =~ /(\bexists|^File is password encrypted|^Unsupported .*), Skipped\b/s;
  13514. }
  13515. defined $ln || $!==0 || $!==EAGAIN or die "Error reading: $!";
  13516. $err = undef; $proc_fh->close or $err = $!; $retval = retcode($?);
  13517. chdir($TEMPBASE) or die "Can't chdir to $TEMPBASE: $!";
  13518. if (!grep { $_ == $retval } (0,1,3)) { # not one of: success, warn, CRC err
  13519. do_log(0, "unarj: error extracting: ".exit_status_str($?,$err));
  13520. }
  13521. # add attributes to the parent object, because we didn't remember names
  13522. # of its scrambled members
  13523. $part->attributes_add('U') if $skippedcount;
  13524. $part->attributes_add('C') if $encryptedcount;
  13525. my($errn) = lstat("$tempdir/parts/arj") ? 0 : 0+$!;
  13526. if ($errn != ENOENT) {
  13527. my($b) = flatten_and_tidy_dir("$tempdir/parts/arj","$tempdir/parts",$part);
  13528. consumed_bytes($b, 'do_unarj');
  13529. snmp_count("OpsDecBy\u${decompressor_name}");
  13530. }
  13531. unlink($part->full_name.".arj")
  13532. or die "Can't unlink " . $part->full_name . ".arj: $!";
  13533. if (!grep { $_ == $retval } (0,1,3)) { # not one of: success, warn, CRC err
  13534. die ("unarj: can't extract archive members: ".exit_status_str($?,$err));
  13535. }
  13536. if ($encryptedcount || $skippedcount) {
  13537. do_log(1, sprintf(
  13538. "do_unarj: %s, %d members are encrypted, %d skipped, archive retained",
  13539. $part->base_name, $encryptedcount, $skippedcount));
  13540. return 2;
  13541. }
  13542. 1;
  13543. }
  13544. # use external program to expand TNEF archives
  13545. sub do_tnef_ext($$$) {
  13546. my($part, $tempdir, $archiver) = @_;
  13547. do_log(4, "Extracting from TNEF encapsulation (ext) " . $part->base_name);
  13548. my($archiver_name) = basename((split(' ',$archiver))[0]);
  13549. snmp_count("OpsDecBy\u${archiver_name}");
  13550. mkdir("$tempdir/parts/tnef",0750)
  13551. or die "Can't mkdir $tempdir/parts/tnef: $!";
  13552. my($proc_fh,$pid) = run_command(undef, "&1", $archiver, '--number-backups',
  13553. '-C', "$tempdir/parts/tnef", '-f', $part->full_name);
  13554. my($nbytes,$buff); my($output) = '';
  13555. while (($nbytes=$proc_fh->read($buff,4096)) > 0) { $output .= $buff }
  13556. defined $nbytes or die "Error reading: $!";
  13557. my($err); $proc_fh->close or $err = $!;
  13558. $?==0 or do_log(0, 'tnef '.exit_status_str($?,$err).' '.$output);
  13559. my($b) = flatten_and_tidy_dir("$tempdir/parts/tnef","$tempdir/parts",$part);
  13560. if ($b > 0) {
  13561. do_log(4, "tnef extracted $b bytes from a tnef container");
  13562. consumed_bytes($b, 'do_tnef');
  13563. }
  13564. 1;
  13565. }
  13566. # use Convert-TNEF
  13567. sub do_tnef($$) {
  13568. my($part, $tempdir) = @_;
  13569. do_log(4, "Extracting from TNEF encapsulation (int) " . $part->base_name);
  13570. snmp_count('OpsDecByTnef');
  13571. my($tnef) = Convert::TNEF->read_in($part->full_name,
  13572. {output_dir=>"$tempdir/parts", buffer_size=>16384, ignore_checksum=>1});
  13573. defined $tnef or die "Convert::TNEF failed: ".$Convert::TNEF::errstr;
  13574. my($item_num) = 0; my($parent_placement) = $part->mime_placement;
  13575. for my $a ($tnef->message, $tnef->attachments) {
  13576. for my $attr_name ('AttachData','Attachment') {
  13577. my($dh) = $a->datahandle($attr_name);
  13578. if (defined $dh) {
  13579. my($newpart_obj)= Amavis::Unpackers::Part->new("$tempdir/parts",$part);
  13580. $item_num++;
  13581. $newpart_obj->mime_placement("$parent_placement/$item_num");
  13582. $newpart_obj->name_declared([$a->name, $a->longname]);
  13583. my($newpart) = $newpart_obj->full_name;
  13584. my($outpart) = IO::File->new;
  13585. $outpart->open($newpart, O_CREAT|O_EXCL|O_WRONLY, 0640)
  13586. or die "Can't create file $newpart: $!";
  13587. binmode($outpart) or die "Can't set file $newpart to binmode: $!";
  13588. my($file) = $dh->path; my($size) = 0;
  13589. if (defined $file) {
  13590. my($io,$nbytes,$buff); $dh->binmode(1);
  13591. $io = $dh->open("r") or die "Can't open MIME::Body handle: $!";
  13592. while (($nbytes=$io->read($buff,16384)) > 0) {
  13593. $outpart->print($buff) or die "Can't write to $newpart: $!";
  13594. $size += $nbytes; consumed_bytes($nbytes, 'do_tnef_1');
  13595. }
  13596. defined $nbytes or die "Error reading from MIME::Body handle: $!";
  13597. $io->close or die "Error closing MIME::Body handle: $!";
  13598. } else {
  13599. my($buff) = $dh->as_string; my($nbytes) = length($buff);
  13600. $outpart->print($buff) or die "Can't write to $newpart: $!";
  13601. $size += $nbytes; consumed_bytes($nbytes, 'do_tnef_2');
  13602. }
  13603. $newpart_obj->size($size);
  13604. $outpart->close or die "Error closing $newpart: $!";
  13605. }
  13606. }
  13607. }
  13608. $tnef->purge if defined $tnef;
  13609. 1;
  13610. }
  13611. # The pax and cpio utilities usually support the following archive formats:
  13612. # cpio, bcpio, sv4cpio, sv4crc, tar (old tar), ustar (POSIX.2 tar).
  13613. # The utilities from http://heirloom.sourceforge.net/ support
  13614. # several other tar/cpio variants such as SCO, Sun, DEC, Cray, SGI
  13615. sub do_pax_cpio($$$) {
  13616. my($part, $tempdir, $archiver) = @_;
  13617. my($archiver_name) = basename((split(' ',$archiver))[0]);
  13618. snmp_count("OpsDecBy\u${archiver_name}");
  13619. ll(4) && do_log(4,sprintf("Expanding archive %s, using %s",
  13620. $part->base_name, $archiver_name));
  13621. my($is_pax) = $archiver_name =~ /^cpio/i ? 0 : 1;
  13622. do_log(-1,"WARN: Using $archiver_name instead of pax can be a security ".
  13623. "risk; please add: \$pax='pax'; to amavisd.conf and check that ".
  13624. "the pax(1) utility is available on the system!") if !$is_pax;
  13625. my(@cmdargs) = $is_pax ? qw(-v) : qw(-i -t -v);
  13626. my($proc_fh,$pid) = run_command($part->full_name, undef, $archiver,@cmdargs);
  13627. my($bytes) = 0; local($1,$2,$3); local($_);
  13628. for (undef $!; defined($_=$proc_fh->getline); undef $!) {
  13629. chomp;
  13630. next if /^\d+ blocks\z/;
  13631. last if /^(cpio|pax): (.*bytes read|End of archive volume)/;
  13632. if (!/^ (?: \S+\s+ ){4}
  13633. (\d+) \s+
  13634. ( (?: \s* \S+ ){3} (?: \s+ \d{4}, )? ) \s+
  13635. (.+) \z/xs) {
  13636. do_log(-1,"do_pax_cpio: can't parse toc line: $_");
  13637. } else {
  13638. my($mem,$size) = ($3,$1);
  13639. $mem = $1 if $is_pax && $mem =~ /^(.*) =[=>] (.*)\z/; # hard or soft link
  13640. do_log(5,"do_pax_cpio: member: \"$mem\", size: $size");
  13641. $bytes += $size if $size > 0;
  13642. }
  13643. }
  13644. defined $_ || $!==0 || $!==EAGAIN or die "Error reading: $!";
  13645. # consume remaining output to avoid broken pipe
  13646. my($nbytes,$buff);
  13647. while (($nbytes=$proc_fh->read($buff,4096)) > 0) { }
  13648. defined $nbytes or die "Error reading: $!";
  13649. my($err); $proc_fh->close or $err = $!;
  13650. $?==0 or do_log(-1, 'do_pax_cpio/1: '.exit_status_str($?,$err));
  13651. consumed_bytes($bytes, 'do_pax_cpio/pre', 1); # pre-check on estimated size
  13652. mkdir("$tempdir/parts/arch", 0750)
  13653. or die "Can't mkdir $tempdir/parts/arch: $!";
  13654. my($name_clash) = 0;
  13655. my(%orig_names); # maps filenames to archive member names when possible
  13656. eval {
  13657. chdir("$tempdir/parts/arch")
  13658. or die "Can't chdir to $tempdir/parts/arch: $!";
  13659. my(@cmdargs) = $is_pax ? qw(-r -k -p am -s /[^A-Za-z0-9_]/-/gp)
  13660. : qw(-i -d --no-absolute-filenames --no-preserve-owner);
  13661. my($proc_fh,$pid) = run_command($part->full_name,"&1",$archiver,@cmdargs);
  13662. my($output) = ''; my($ln);
  13663. for (undef $!; defined($ln=$proc_fh->getline); undef $!) {
  13664. chomp($ln);
  13665. if (!$is_pax || $ln !~ /^(.*) >> (\S*)\z/) { $output .= $ln."\n" }
  13666. else { # parse output from pax -s///p
  13667. my($member_name,$file_name) = ($1,$2);
  13668. if (!exists $orig_names{$file_name}) {
  13669. $orig_names{$file_name} = $member_name;
  13670. } else {
  13671. do_log(0,sprintf("do_pax_cpio: member \"%s\" is hidden by a ".
  13672. "previous archive member \"%s\", file: %s",
  13673. $member_name, $orig_names{$file_name}, $file_name));
  13674. $orig_names{$file_name} = undef; # cause it to exist but undefined
  13675. $name_clash++;
  13676. }
  13677. }
  13678. }
  13679. defined $ln || $!==0 || $!==EAGAIN or die "Error reading: $!";
  13680. chomp($output); my($err); $proc_fh->close or $err = $!;
  13681. $?==0 or die (exit_status_str($?,$err).' '.$output);
  13682. };
  13683. my($eval_stat) = $@;
  13684. chdir($TEMPBASE) or die "Can't chdir to $TEMPBASE: $!";
  13685. my($b) = flatten_and_tidy_dir("$tempdir/parts/arch", "$tempdir/parts",
  13686. $part, 0, \%orig_names);
  13687. consumed_bytes($b, 'do_pax_cpio');
  13688. if ($eval_stat ne '') { chomp($eval_stat); die "do_pax_cpio: $eval_stat\n" }
  13689. $name_clash ? 2 : 1;
  13690. }
  13691. # ar is a standard Unix binary archiver, also used by Debian packages
  13692. sub do_ar($$$) {
  13693. my($part, $tempdir, $archiver) = @_;
  13694. ll(4) && do_log(4,"Expanding Unix ar archive ".$part->full_name);
  13695. my($archiver_name) = basename((split(' ',$archiver))[0]);
  13696. snmp_count("OpsDecBy\u${archiver_name}");
  13697. my($proc_fh,$pid) = run_command(undef,undef,$archiver,'tv',$part->full_name);
  13698. my($ln); my($bytes) = 0; local($1,$2,$3);
  13699. for (undef $!; defined($ln=$proc_fh->getline); undef $!) {
  13700. chomp($ln);
  13701. if ($ln !~ /^(?:\S+\s+){2}(\d+)\s+((?:\S+\s+){3}\S+)\s+(.*)\z/) {
  13702. do_log(-1,"do_ar: can't parse contents listing line: $ln");
  13703. } else {
  13704. do_log(5,"do_ar: member: \"$3\", size: $1");
  13705. $bytes += $1 if $1 > 0;
  13706. }
  13707. }
  13708. defined $ln || $!==0 || $!==EAGAIN or die "Error reading: $!";
  13709. # consume remaining output to avoid broken pipe
  13710. my($nbytes,$buff);
  13711. while (($nbytes=$proc_fh->read($buff,4096)) > 0) { }
  13712. defined $nbytes or die "Error reading: $!";
  13713. my($err); $proc_fh->close or $err = $!;
  13714. $?==0 or do_log(-1, 'ar-1 '.exit_status_str($?,$err));
  13715. consumed_bytes($bytes, 'do_ar-pre', 1); # pre-check on estimated size
  13716. mkdir("$tempdir/parts/ar", 0750)
  13717. or die "Can't mkdir $tempdir/parts/ar: $!";
  13718. chdir("$tempdir/parts/ar") or die "Can't chdir to $tempdir/parts/ar: $!";
  13719. ($proc_fh,$pid) = run_command(undef, "&1", $archiver, 'x', $part->full_name);
  13720. my($output) = '';
  13721. while (($nbytes=$proc_fh->read($buff,4096)) > 0) { $output .= $buff }
  13722. defined $nbytes or die "Error reading: $!";
  13723. $err = undef; $proc_fh->close or $err = $!;
  13724. $?==0 or do_log(-1, 'ar-2 '.exit_status_str($?,$err).' '.$output);
  13725. chdir($TEMPBASE) or die "Can't chdir to $TEMPBASE: $!";
  13726. my($b) = flatten_and_tidy_dir("$tempdir/parts/ar","$tempdir/parts",$part);
  13727. consumed_bytes($b, 'do_ar');
  13728. 1;
  13729. }
  13730. sub do_cabextract($$$) {
  13731. my($part, $tempdir, $archiver) = @_;
  13732. do_log(4, "Expanding cab archive " . $part->base_name);
  13733. my($archiver_name) = basename((split(' ',$archiver))[0]);
  13734. snmp_count("OpsDecBy\u${archiver_name}");
  13735. local($_); my($bytes) = 0; my($ln);
  13736. my($proc_fh,$pid) =
  13737. run_command(undef,undef,$archiver,'-l',$part->full_name);
  13738. for (undef $!; defined($ln=$proc_fh->getline); undef $!) {
  13739. chomp($ln);
  13740. next if $ln =~ /^(File size|----|Viewing cabinet:|\z)/;
  13741. if ($ln !~ /^\s* (\d+) \s* \| [^|]* \| \s (.*) \z/x) {
  13742. do_log(-1, "do_cabextract: can't parse toc line: $ln");
  13743. } else {
  13744. do_log(5, "do_cabextract: member: \"$2\", size: $1");
  13745. $bytes += $1 if $1 > 0;
  13746. }
  13747. }
  13748. defined $ln || $!==0 || $!==EAGAIN or die "Error reading: $!";
  13749. # consume remaining output to avoid broken pipe (just in case)
  13750. my($nbytes,$buff);
  13751. while (($nbytes=$proc_fh->read($buff,4096)) > 0) { }
  13752. defined $nbytes or die "Error reading: $!";
  13753. my($err); $proc_fh->close or $err = $!;
  13754. $?==0 or do_log(-1, 'cabextract-1 '.exit_status_str($?,$err));
  13755. consumed_bytes($bytes, 'do_cabextract-pre', 1); # pre-check on estimated size
  13756. mkdir("$tempdir/parts/cab", 0750) or die "Can't mkdir $tempdir/parts/cab: $!";
  13757. ($proc_fh,$pid) = run_command(undef, '/dev/null', $archiver, '-q', '-d',
  13758. "$tempdir/parts/cab", $part->full_name);
  13759. my($output) = '';
  13760. while (($nbytes=$proc_fh->read($buff,4096)) > 0) { $output .= $buff }
  13761. defined $nbytes or die "Error reading: $!";
  13762. $err = undef; $proc_fh->close or $err = $!;
  13763. $?==0 or do_log(-1, 'cabextract-2 '.exit_status_str($?,$err).' '.$output);
  13764. my($b) = flatten_and_tidy_dir("$tempdir/parts/cab", "$tempdir/parts", $part);
  13765. consumed_bytes($b, 'do_cabextract');
  13766. 1;
  13767. }
  13768. sub do_ole($$$) {
  13769. my($part, $tempdir, $archiver) = @_;
  13770. do_log(4,"Expanding MS OLE document " . $part->base_name);
  13771. my($archiver_name) = basename((split(' ',$archiver))[0]);
  13772. snmp_count("OpsDecBy\u${archiver_name}");
  13773. mkdir("$tempdir/parts/ole",0750) or die "Can't mkdir $tempdir/parts/ole: $!";
  13774. my($proc_fh,$pid) = run_command(undef, "&1", $archiver, '-v',
  13775. '-i', $part->full_name, '-d',"$tempdir/parts/ole");
  13776. my($nbytes,$buff); my($output) = '';
  13777. while (($nbytes=$proc_fh->read($buff,4096)) > 0) { $output .= $buff }
  13778. defined $nbytes or die "Error reading: $!";
  13779. my($err); $proc_fh->close or $err = $!;
  13780. $?==0 or do_log(0, 'ripOLE '.exit_status_str($?,$err).' '.$output);
  13781. my($b) = flatten_and_tidy_dir("$tempdir/parts/ole", "$tempdir/parts", $part);
  13782. if ($b > 0) {
  13783. do_log(4, "ripOLE extracted $b bytes from an OLE document");
  13784. consumed_bytes($b, 'do_ole');
  13785. }
  13786. 2; # always keep the original OLE document
  13787. }
  13788. # Check for self-extracting archives. Note that we don't rely on
  13789. # file magic here since it's not reliable. Instead we will try each
  13790. # archiver.
  13791. sub do_executable($$@) {
  13792. my($part, $tempdir, $unrar, $lha, $unarj) = @_;
  13793. ll(4) && do_log(4,"Check whether ".$part->base_name.
  13794. " is a self-extracting archive");
  13795. # ZIP?
  13796. return 2 if eval { do_unzip($part,$tempdir) };
  13797. chomp($@);
  13798. do_log(-1,"do_executable/do_unzip failed, ignoring: $@") if $@ ne '';
  13799. # RAR?
  13800. return 2 if defined $unrar && eval { do_unrar($part,$tempdir,$unrar) };
  13801. chomp($@);
  13802. do_log(-1,"do_executable/do_unrar failed, ignoring: $@") if $@ ne '';
  13803. # LHA?
  13804. return 2 if defined $lha && eval { do_lha($part,$tempdir,$lha) };
  13805. chomp($@);
  13806. do_log(-1,"do_executable/do_lha failed, ignoring: $@") if $@ ne '';
  13807. # # ARJ?
  13808. # return 2 if defined $unarj && eval { do_unarj($part,$tempdir,$unarj) };
  13809. # chomp($@);
  13810. # do_log(-1,"do_executable/do_unarj failed, ignoring: $@") if $@ ne '';
  13811. return 0;
  13812. }
  13813. # my($k,$v,$fn);
  13814. # while (($k,$v) = each(%::)) {
  13815. # local(*e)=$v; $fn=fileno(\*e);
  13816. # printf STDERR ("%-10s %-10s %s$eol",$k,$v,$fn) if defined $fn;
  13817. # }
  13818. # Given a file handle (typically opened pipe to a subprocess, as returned
  13819. # from run_command), copy from it to a specified output file in binary mode.
  13820. sub run_command_copy($$) {
  13821. my($outfile, $ifh) = @_;
  13822. my($ofh) = IO::File->new;
  13823. $ofh->open($outfile, O_CREAT|O_EXCL|O_WRONLY, 0640)
  13824. or die "Can't create file $outfile: $!";
  13825. binmode($ofh) or die "Can't set file $outfile to binmode: $!";
  13826. binmode($ifh) or die "Can't set binmode on pipe: $!";
  13827. my($len, $buf, $offset, $written);
  13828. for ($!=0; ($len=$ifh->sysread($buf,16384)) > 0; $!=0) {
  13829. $offset = 0;
  13830. while ($len > 0) { # handle partial writes
  13831. $written = syswrite($ofh, $buf, $len, $offset);
  13832. defined($written) or die "syswrite to $outfile failed: $!";
  13833. consumed_bytes($written, 'run_command_copy');
  13834. $len -= $written; $offset += $written;
  13835. }
  13836. }
  13837. my($rv,$rerr); $rerr = 0;
  13838. if (defined $len || $!==0) { $ifh->close or $rerr = $! } # ok
  13839. else { $rerr = $!; $ifh->close } # remember error, ignore stat on close
  13840. $rv = $?;
  13841. $ofh->close or die "Error closing $outfile: $!";
  13842. ($rv,$rerr); # return subprocess termination status and reading/close errno
  13843. }
  13844. # extract listed files from archive and store in new file
  13845. sub store_mgr($$$@) {
  13846. my($tempdir, $parent_obj, $list, $cmd, @args) = @_;
  13847. my($item_num) = 0; my($parent_placement) = $parent_obj->mime_placement;
  13848. my($result_status) = 0;
  13849. for my $f (@$list) {
  13850. next if $f =~ m{/\z}; # ignore directories
  13851. my($newpart_obj) =
  13852. Amavis::Unpackers::Part->new("$tempdir/parts",$parent_obj);
  13853. $item_num++; $newpart_obj->mime_placement("$parent_placement/$item_num");
  13854. $newpart_obj->name_declared($f); # store tainted name
  13855. my($newpart) = $newpart_obj->full_name;
  13856. ll(5) && do_log(5,sprintf('store_mgr: extracting "%s" to file %s using %s',
  13857. $f, $newpart, $cmd));
  13858. if ($f =~ m{^\.?[A-Za-z0-9_][A-Za-z0-9/._=~-]*\z}) { # apparently safe arg
  13859. } else { # this is not too bad, as run_command does not use shell
  13860. do_log(1, "store_mgr: NOTICE: untainting funny argument \"$f\"");
  13861. }
  13862. my($proc_fh,$pid) = run_command(undef,undef,$cmd,@args,untaint($f));
  13863. my($rv,$rerr) = run_command_copy($newpart,$proc_fh);
  13864. my($ll) = $rv!=0 || $rerr!= 0 ? 1 : 5;
  13865. ll($ll) && do_log($ll,"store_mgr: extracted by $cmd, ".
  13866. exit_status_str($rv,$rerr));
  13867. $result_status = $rv if $result_status == 0 && $rv != 0;
  13868. }
  13869. $result_status; # return the first nonzero status (if any), or 0
  13870. }
  13871. 1;
  13872. __DATA__
  13873. #
  13874. # =============================================================================
  13875. # This text section governs how a main per-message amavisd-new log entry
  13876. # is formed. An empty text will prevent a log entry, multi-line text will
  13877. # produce several log entries, one for each nonempty line.
  13878. # Syntax is explained in the README.customize file.
  13879. [?%#D|#|Passed #
  13880. [? [?%#V|1] |INFECTED (%V)|#
  13881. [? [?%#F|1] |BANNED (%F)|#
  13882. [? [? %2|1] |SPAM|#
  13883. [? [?%#X|1] |BAD-HEADER|CLEAN]]]]#
  13884. , [? %p ||%p ][?%a||[?%l||LOCAL ]\[%a\] ][?%e||\[%e\] ]<%o> -> [%D|,]#
  13885. [? %q ||, quarantine: %q]#
  13886. [? %Q ||, Queue-ID: %Q]#
  13887. [? %m ||, Message-ID: %m]#
  13888. [? %r ||, Resent-Message-ID: %r]#
  13889. , mail_id: %i#
  13890. , Hits: %c#
  13891. #, size: %z#
  13892. #[? %j ||, Subject: "%j"]#
  13893. #[? %#T ||, Tests: \[[%T|,]]\]#
  13894. , %y ms#
  13895. ]
  13896. [?%#O|#|Blocked #
  13897. [? [?%#V|1] |INFECTED (%V)|#
  13898. [? [?%#F|1] |BANNED (%F)|#
  13899. [? [? %2|1] |SPAM|#
  13900. [? [?%#X|1] |BAD-HEADER|CLEAN]]]]#
  13901. , [? %p ||%p ][?%a||[?%l||LOCAL ]\[%a\] ][?%e||\[%e\] ]<%o> -> [%O|,]#
  13902. [? %q ||, quarantine: %q]#
  13903. [? %Q ||, Queue-ID: %Q]#
  13904. [? %m ||, Message-ID: %m]#
  13905. [? %r ||, Resent-Message-ID: %r]#
  13906. , mail_id: %i#
  13907. , Hits: %c#
  13908. #, size: %z#
  13909. #[? %j ||, Subject: "%j"]#
  13910. #[? %#T ||, Tests: \[[%T|,]]\]#
  13911. , %y ms#
  13912. ]
  13913. __DATA__
  13914. #
  13915. # =============================================================================
  13916. # This text section governs how a main per-recipient amavisd-new log entry
  13917. # is formed. An empty text will prevent a log entry, multi-line text will
  13918. # produce several log entries, one for each nonempty line.
  13919. # Macro %. might be useful, it counts recipients starting from 1.
  13920. # Syntax is explained in the README.customize file.
  13921. #
  13922. [?%#D|#|Passed #
  13923. [? [?%#V|1] |INFECTED (%V)|#
  13924. [? [?%#F|1] |BANNED (%F)|#
  13925. [? [? %2|1] |SPAM|#
  13926. [? [?%#X|1] |BAD-HEADER|CLEAN]]]]#
  13927. , <%o> -> [%D|,], Hits: %c#
  13928. , tag=%3, tag2=%4, kill=%5# NOTE: macros %3, %4, %5 are experimental
  13929. , %0/%1/%2/%k#
  13930. ]
  13931. [?%#O|#|Blocked #
  13932. [? [?%#V|1] |INFECTED (%V)|#
  13933. [? [?%#F|1] |BANNED (%F)|#
  13934. [? [? %2|1] |SPAM|#
  13935. [? [?%#X|1] |BAD-HEADER|CLEAN]]]]#
  13936. , <%o> -> [%O|,], Hits: %c#
  13937. , tag=%3, tag2=%4, kill=%5# NOTE: macros %3, %4, %5 are experimental
  13938. , %0/%1/%2/%k#
  13939. ]
  13940. __DATA__
  13941. #
  13942. # =============================================================================
  13943. # This is a template for (neutral: non-virus, non-spam, non-banned) DELIVERY
  13944. # STATUS NOTIFICATIONS to sender. For syntax and customization instructions
  13945. # see README.customize. Note that only valid header fields are allowed;
  13946. # non-standard header field heads must begin with "X-" .
  13947. # The From, To and Date header fields will be provided automatically.
  13948. #
  13949. Subject: [?%#D|Undeliverable mail|Delivery warning][?%#X||, invalid characters in header]
  13950. Message-ID: <DSN%i@%h>
  13951. [? %#X ||INVALID HEADER (INVALID CHARACTERS OR SPACE GAP)
  13952. [%X\n]
  13953. ]\
  13954. This [?%#D|nondelivery|delivery] report was generated by the amavisd-new program
  13955. at host %h. Our internal reference code for your message
  13956. is %n/%i.
  13957. [? %#X ||
  13958. WHAT IS AN INVALID CHARACTER IN MAIL HEADER?
  13959. The RFC 2822 standard specifies rules for forming internet messages.
  13960. It does not allow the use of characters with codes above 127 to be used
  13961. directly (non-encoded) in mail header (it also prohibits NUL and bare CR).
  13962. If characters (e.g. with diacritics) from ISO Latin or other alphabets
  13963. need to be included in the header, these characters need to be properly
  13964. encoded according to RFC 2047. This encoding is often done transparently
  13965. by mail reader (MUA), but if automatic encoding is not available (e.g.
  13966. by some older MUA) it is the user's responsibility to avoid the use
  13967. of such characters in mail header, or to encode them manually. Typically
  13968. the offending header fields in this category are 'Subject', 'Organization',
  13969. and comment fields in e-mail addresses of the 'From', 'To' and 'Cc'.
  13970. Sometimes such invalid header fields are inserted automatically
  13971. by some MUA, MTA, content checker, or other mail handling service.
  13972. If this is the case, that service needs to be fixed or properly configured.
  13973. Typically the offending header fields in this category are 'Date',
  13974. 'Received', 'X-Mailer', 'X-Priority', 'X-Scanned', etc.
  13975. If you don't know how to fix or avoid the problem, please report it
  13976. to _your_ postmaster or system manager.
  13977. ]\
  13978. Return-Path: %s
  13979. Your message[?%m|| %m][?%r|| (Resent-Message-ID: %r)]
  13980. [?%#D|could not be|was] delivered to:[\n %N]
  13981. __DATA__
  13982. #
  13983. # =============================================================================
  13984. # This is a template for VIRUS/BANNED SENDER NOTIFICATIONS.
  13985. # For syntax and customization instructions see README.customize.
  13986. # Note that only valid header fields are allowed;
  13987. # non-standard header field heads must begin with "X-" .
  13988. # The From, To and Date header fields will be provided automatically.
  13989. #
  13990. Subject: [? %#V |[? %#F |Unknown problem|BANNED (%F)]|VIRUS (%V)] IN MAIL FROM YOU
  13991. [? %m |#|In-Reply-To: %m]
  13992. Message-ID: <VS%i@%h>
  13993. [? %#V |[? %#F |[? %#X ||INVALID HEADER]|BANNED CONTENTS ALERT]|VIRUS ALERT]
  13994. Our content checker found
  13995. [? %#V |#| [? %#V |viruses|virus|viruses]: %V]
  13996. [? %#F |#| banned [? %#F |names|name|names]: %F]
  13997. [? %#X |#|\n[%X\n]]
  13998. in email presumably from you (%s),
  13999. to the following [? %#R |recipients|recipient|recipients]:[
  14000. -> %R]
  14001. [? %a |#|First upstream SMTP client IP address: \[%a\] %g]
  14002. [? %e |#|According to the 'Received:' trace, the message originated at: \[%e\]]
  14003. Our internal reference code for your message is %n/%i.
  14004. [? %#V ||Please check your system for viruses,
  14005. or ask your system administrator to do so.
  14006. ]#
  14007. [? %#D |Delivery of the email was stopped!
  14008. ]#
  14009. [? %#V |[? %#F ||#
  14010. The message [?%#D|has been blocked|triggered this warning] because it contains a component
  14011. (as a MIME part or nested within) with declared name
  14012. or MIME type or contents type violating our access policy.
  14013. To transfer contents that may be considered risky or unwanted
  14014. by site policies, or simply too large for mailing, please consider
  14015. publishing your content on the web, and only sending an URL of the
  14016. document to the recipient.
  14017. Depending on the recipient and sender site policies, with a little
  14018. effort it might still be possible to send any contents (including
  14019. viruses) using one of the following methods:
  14020. - encrypted using pgp, gpg or other encryption methods;
  14021. - wrapped in a password-protected or scrambled container or archive
  14022. (e.g.: zip -e, arj -g, arc g, rar -p, or other methods)
  14023. Note that if the contents is not intended to be secret, the
  14024. encryption key or password may be included in the same message
  14025. for recipient's convenience.
  14026. We are sorry for inconvenience if the contents was not malicious.
  14027. The purpose of these restrictions is to cut the most common propagation
  14028. methods used by viruses and other malware. These often exploit automatic
  14029. mechanisms and security holes in more popular mail readers (Microsoft
  14030. mail readers and browsers are a common target). By requiring an explicit
  14031. and decisive action from the recipient to decode mail, the dangers of
  14032. automatic malware propagation is largely reduced.
  14033. #
  14034. # Details of our mail restrictions policy are available at ...
  14035. ]]#
  14036. For your reference, here are headers from your email:
  14037. ------------------------- BEGIN HEADERS -----------------------------
  14038. Return-Path: %s
  14039. [%H
  14040. ]\
  14041. -------------------------- END HEADERS ------------------------------
  14042. __DATA__
  14043. #
  14044. # =============================================================================
  14045. # This is a template for non-spam (VIRUS,...) ADMINISTRATOR NOTIFICATIONS.
  14046. # For syntax and customization instructions see README.customize.
  14047. # Note that only valid header fields are allowed; non-standard header
  14048. # field heads must begin with "X-" .
  14049. #
  14050. Date: %d
  14051. From: %f
  14052. Subject: [? %#V |[? %#F |[? %#X ||INVALID HEADER]|BANNED (%F)]|VIRUS (%V)]#
  14053. FROM [?%l||LOCAL ][?%a||\[%a\] ][?%o|(?)|<%o>]
  14054. To: [? %#T |undisclosed-recipients: ;|[<%T>|, ]]
  14055. [? %#C |#|Cc: [<%C>|, ]]
  14056. Message-ID: <VA%i@%h>
  14057. [? %#V |No viruses were found.
  14058. |A virus was found: %V
  14059. |Two viruses were found:\n %V
  14060. |%#V viruses were found:\n %V
  14061. ]
  14062. [? %#F |#\
  14063. |A banned name was found:\n %F
  14064. |Two banned names were found:\n %F
  14065. |%#F banned names were found:\n %F
  14066. ]
  14067. [? %#X |#\
  14068. |Bad header was found:[\n %X]
  14069. ]
  14070. [? %#W |#\
  14071. |Scanner detecting a virus: %W
  14072. |Scanners detecting a virus: %W
  14073. ]
  14074. Our internal reference code for the message is %n/%i.
  14075. The mail originated from: <%o>
  14076. [? %a |#|First upstream SMTP client IP address: \[%a\] %g
  14077. ]
  14078. [? %t |#|According to the 'Received:' trace, the message originated at:
  14079. \[%e\]
  14080. %t
  14081. ]
  14082. [? %#S |Notification to sender will not be mailed.
  14083. ]#
  14084. [? %#D |#|The message WILL BE delivered to:[\n%D]
  14085. ]
  14086. [? %#N |#|The message WAS NOT delivered to:[\n%N]
  14087. ]
  14088. [? %#V |#|[? %#v |#|Virus scanner output:[\n %v]
  14089. ]]
  14090. [? %q |Not quarantined.|The message has been quarantined as:\n %q
  14091. ]
  14092. ------------------------- BEGIN HEADERS -----------------------------
  14093. Return-Path: %s
  14094. [%H
  14095. ]\
  14096. -------------------------- END HEADERS ------------------------------
  14097. __DATA__
  14098. #
  14099. # =============================================================================
  14100. # This is a template for VIRUS/BANNED/BAD-HEADER RECIPIENTS NOTIFICATIONS.
  14101. # For syntax and customization instructions see README.customize.
  14102. # Note that only valid header fields are allowed; non-standard header
  14103. # field heads must begin with "X-" .
  14104. #
  14105. Date: %d
  14106. From: %f
  14107. Subject: [? %#V |[? %#F |[? %#X ||INVALID HEADER]|BANNED]|VIRUS (%V)]#
  14108. IN MAIL TO YOU (from [?%o|(?)|<%o>])
  14109. To: [? %#T |undisclosed-recipients: ;|[<%T>|, ]]
  14110. [? %#C |#|Cc: [<%C>|, ]]
  14111. Message-ID: <VR%i@%h>
  14112. [? %#V |[? %#F ||BANNED CONTENTS ALERT]|VIRUS ALERT]
  14113. Our content checker found
  14114. [? %#V |#| [? %#V |viruses|virus|viruses]: %V]
  14115. [? %#F |#| banned [? %#F |names|name|names]: %F]
  14116. [? %#X |#|\n[%X\n]]
  14117. in an email to you [? %S |from unknown sender:|from:]
  14118. %o
  14119. [? %S |claiming to be: %s|#]
  14120. [? %a |#|First upstream SMTP client IP address: \[%a\] %g
  14121. ]
  14122. [? %t |#|According to the 'Received:' trace, the message originated at:
  14123. \[%e\]
  14124. %t
  14125. ]
  14126. Our internal reference code for the message is %n/%i.
  14127. [? %q |Not quarantined.|The message has been quarantined as:
  14128. %q]
  14129. Please contact your system administrator for details.
  14130. __DATA__
  14131. #
  14132. # =============================================================================
  14133. # This is a template for SPAM SENDER NOTIFICATIONS.
  14134. # For syntax and customization instructions see README.customize.
  14135. # Note that only valid header fields are allowed;
  14136. # non-standard header field heads must begin with "X-" .
  14137. # The From, To and Date header fields will be provided automatically.
  14138. #
  14139. Subject: Considered UNSOLICITED BULK EMAIL from you
  14140. [? %m |#|In-Reply-To: %m]
  14141. Message-ID: <SS%i@%h>
  14142. Your message to:[
  14143. -> %R]
  14144. was considered unsolicited bulk e-mail (UBE).
  14145. [? %#X |#|\n[%X\n]]
  14146. Subject: %j
  14147. Return-Path: %s
  14148. [? %a |#|First upstream SMTP client IP address: \[%a\] %g]
  14149. [? %e |#|According to the 'Received:' trace, the message originated at: \[%e\]]
  14150. Our internal reference code for your message is %n/%i.
  14151. [? %#D |Delivery of the email was stopped!
  14152. ]#
  14153. #
  14154. # SpamAssassin report:
  14155. # [%A
  14156. # ]\
  14157. __DATA__
  14158. #
  14159. # =============================================================================
  14160. # This is a template for SPAM ADMINISTRATOR NOTIFICATIONS.
  14161. # For syntax and customization instructions see README.customize.
  14162. # Note that only valid header fields are allowed; non-standard header
  14163. # field heads must begin with "X-" .
  14164. #
  14165. Date: %d
  14166. From: %f
  14167. Subject: SPAM FROM [?%l||LOCAL ][?%a||\[%a\] ][?%o|(?)|<%o>]
  14168. To: [? %#T |undisclosed-recipients: ;|[<%T>|, ]]
  14169. [? %#C |#|Cc: [<%C>|, ]]
  14170. [? %#B |#|Bcc: [<%B>|, ]]
  14171. Message-ID: <SA%i@%h>
  14172. Unsolicited bulk email [? %S |from unknown or forged sender:|from:]
  14173. %o
  14174. Subject: %j
  14175. Our internal reference code for the message is %n/%i.
  14176. [? %a |#|First upstream SMTP client IP address: \[%a\] %g
  14177. ]
  14178. [? %t |#|According to the 'Received:' trace, the message originated at:
  14179. \[%e\]
  14180. %t
  14181. ]
  14182. [? %#D |#|The message WILL BE delivered to:[\n%D]
  14183. ]
  14184. [? %#N |#|The message WAS NOT delivered to:[\n%N]
  14185. ]
  14186. [? %q |Not quarantined.|The message has been quarantined as:\n %q
  14187. ]
  14188. SpamAssassin report:
  14189. [%A
  14190. ]\
  14191. ------------------------- BEGIN HEADERS -----------------------------
  14192. Return-Path: %s
  14193. [%H
  14194. ]\
  14195. -------------------------- END HEADERS ------------------------------