1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162416341644165416641674168416941704171417241734174417541764177417841794180418141824183418441854186418741884189419041914192419341944195419641974198419942004201420242034204420542064207420842094210421142124213421442154216421742184219422042214222422342244225422642274228422942304231423242334234423542364237423842394240424142424243424442454246424742484249425042514252425342544255425642574258425942604261426242634264426542664267426842694270427142724273427442754276427742784279428042814282428342844285428642874288428942904291429242934294429542964297429842994300430143024303430443054306430743084309431043114312431343144315431643174318431943204321432243234324432543264327432843294330433143324333433443354336433743384339434043414342434343444345434643474348434943504351435243534354435543564357435843594360436143624363436443654366436743684369437043714372437343744375437643774378437943804381438243834384438543864387438843894390439143924393439443954396439743984399440044014402440344044405440644074408440944104411441244134414441544164417441844194420442144224423442444254426442744284429443044314432443344344435443644374438443944404441444244434444444544464447444844494450445144524453445444554456445744584459446044614462446344644465446644674468446944704471447244734474447544764477447844794480448144824483448444854486448744884489449044914492449344944495449644974498449945004501450245034504450545064507450845094510451145124513451445154516451745184519452045214522452345244525452645274528452945304531453245334534453545364537453845394540454145424543454445454546454745484549455045514552455345544555455645574558455945604561456245634564456545664567456845694570457145724573457445754576457745784579458045814582458345844585458645874588458945904591459245934594459545964597459845994600460146024603460446054606460746084609461046114612461346144615461646174618461946204621462246234624462546264627462846294630463146324633463446354636463746384639464046414642464346444645464646474648464946504651465246534654465546564657465846594660466146624663466446654666466746684669467046714672467346744675467646774678467946804681468246834684468546864687468846894690469146924693469446954696469746984699470047014702470347044705470647074708470947104711471247134714471547164717471847194720472147224723472447254726472747284729473047314732473347344735473647374738473947404741474247434744474547464747474847494750475147524753475447554756475747584759476047614762476347644765476647674768476947704771477247734774477547764777477847794780478147824783478447854786478747884789479047914792479347944795479647974798479948004801480248034804480548064807480848094810481148124813481448154816481748184819482048214822482348244825482648274828482948304831483248334834483548364837483848394840484148424843484448454846484748484849485048514852485348544855485648574858485948604861486248634864486548664867486848694870487148724873487448754876487748784879488048814882488348844885488648874888488948904891489248934894489548964897489848994900490149024903490449054906490749084909491049114912491349144915491649174918491949204921492249234924492549264927492849294930493149324933493449354936493749384939494049414942494349444945494649474948494949504951495249534954495549564957495849594960496149624963496449654966496749684969497049714972497349744975497649774978497949804981498249834984498549864987498849894990499149924993499449954996499749984999500050015002500350045005500650075008500950105011501250135014501550165017501850195020502150225023502450255026502750285029503050315032503350345035503650375038503950405041504250435044504550465047504850495050505150525053505450555056505750585059506050615062506350645065506650675068506950705071507250735074507550765077507850795080508150825083508450855086508750885089509050915092509350945095509650975098509951005101510251035104510551065107510851095110511151125113511451155116511751185119512051215122512351245125512651275128512951305131513251335134513551365137513851395140514151425143514451455146514751485149515051515152515351545155515651575158515951605161516251635164516551665167516851695170517151725173517451755176517751785179518051815182518351845185518651875188518951905191519251935194519551965197519851995200520152025203520452055206520752085209521052115212521352145215521652175218521952205221522252235224522552265227522852295230523152325233523452355236523752385239524052415242524352445245524652475248524952505251525252535254525552565257525852595260526152625263526452655266526752685269527052715272527352745275527652775278527952805281528252835284528552865287528852895290529152925293529452955296529752985299530053015302530353045305530653075308530953105311531253135314531553165317531853195320532153225323532453255326532753285329533053315332533353345335533653375338533953405341534253435344534553465347534853495350535153525353535453555356535753585359536053615362536353645365536653675368536953705371537253735374537553765377537853795380538153825383538453855386538753885389539053915392539353945395539653975398539954005401540254035404540554065407540854095410541154125413541454155416541754185419542054215422542354245425542654275428542954305431543254335434543554365437543854395440544154425443544454455446544754485449545054515452545354545455545654575458545954605461546254635464546554665467546854695470547154725473547454755476547754785479548054815482548354845485548654875488548954905491549254935494549554965497549854995500550155025503550455055506550755085509551055115512551355145515551655175518551955205521552255235524552555265527552855295530553155325533553455355536553755385539554055415542554355445545554655475548554955505551555255535554555555565557555855595560556155625563556455655566556755685569557055715572557355745575557655775578557955805581558255835584558555865587558855895590559155925593559455955596559755985599560056015602560356045605560656075608560956105611561256135614561556165617561856195620562156225623562456255626562756285629563056315632563356345635563656375638563956405641564256435644564556465647564856495650565156525653565456555656565756585659566056615662566356645665566656675668566956705671567256735674567556765677567856795680568156825683568456855686568756885689569056915692569356945695569656975698569957005701570257035704570557065707570857095710571157125713571457155716571757185719572057215722572357245725572657275728572957305731573257335734573557365737573857395740574157425743574457455746574757485749575057515752575357545755575657575758575957605761576257635764576557665767576857695770577157725773577457755776577757785779578057815782578357845785578657875788578957905791579257935794579557965797579857995800580158025803580458055806580758085809581058115812581358145815581658175818581958205821582258235824582558265827582858295830583158325833583458355836583758385839584058415842584358445845584658475848584958505851585258535854585558565857585858595860586158625863586458655866586758685869587058715872587358745875587658775878587958805881588258835884588558865887588858895890589158925893589458955896589758985899590059015902590359045905590659075908590959105911591259135914591559165917591859195920592159225923592459255926592759285929593059315932593359345935593659375938593959405941594259435944594559465947594859495950595159525953595459555956595759585959596059615962596359645965596659675968596959705971597259735974597559765977597859795980598159825983598459855986598759885989599059915992599359945995599659975998599960006001600260036004600560066007600860096010601160126013601460156016601760186019602060216022602360246025602660276028602960306031603260336034603560366037603860396040604160426043604460456046604760486049605060516052605360546055605660576058605960606061606260636064606560666067606860696070607160726073607460756076607760786079608060816082608360846085608660876088608960906091609260936094609560966097609860996100610161026103610461056106610761086109611061116112611361146115611661176118611961206121612261236124612561266127612861296130613161326133613461356136613761386139614061416142614361446145614661476148614961506151615261536154615561566157615861596160616161626163616461656166616761686169617061716172617361746175617661776178617961806181618261836184618561866187618861896190619161926193619461956196619761986199620062016202620362046205620662076208620962106211621262136214621562166217621862196220622162226223622462256226622762286229623062316232623362346235623662376238623962406241624262436244624562466247624862496250625162526253625462556256625762586259626062616262626362646265626662676268626962706271627262736274627562766277627862796280628162826283628462856286628762886289629062916292629362946295629662976298629963006301630263036304630563066307630863096310631163126313631463156316631763186319632063216322632363246325632663276328632963306331633263336334633563366337633863396340634163426343634463456346634763486349635063516352635363546355635663576358635963606361636263636364636563666367636863696370637163726373637463756376637763786379638063816382638363846385638663876388638963906391639263936394639563966397639863996400640164026403640464056406640764086409641064116412641364146415641664176418641964206421642264236424642564266427642864296430643164326433643464356436643764386439644064416442644364446445644664476448644964506451645264536454645564566457645864596460646164626463646464656466646764686469647064716472647364746475647664776478647964806481648264836484648564866487648864896490649164926493649464956496649764986499650065016502650365046505650665076508650965106511651265136514651565166517651865196520652165226523652465256526652765286529653065316532653365346535653665376538653965406541654265436544654565466547654865496550655165526553655465556556655765586559656065616562656365646565656665676568656965706571657265736574657565766577657865796580658165826583658465856586658765886589659065916592659365946595659665976598659966006601660266036604660566066607660866096610661166126613661466156616661766186619662066216622662366246625662666276628662966306631663266336634663566366637663866396640664166426643664466456646664766486649665066516652665366546655665666576658665966606661666266636664666566666667666866696670667166726673667466756676667766786679668066816682668366846685668666876688668966906691669266936694669566966697669866996700670167026703670467056706670767086709671067116712671367146715671667176718671967206721672267236724672567266727672867296730673167326733673467356736673767386739674067416742674367446745674667476748674967506751675267536754675567566757675867596760676167626763676467656766676767686769677067716772677367746775677667776778677967806781678267836784678567866787678867896790679167926793679467956796679767986799680068016802680368046805680668076808680968106811681268136814681568166817681868196820682168226823682468256826682768286829683068316832683368346835683668376838683968406841684268436844684568466847684868496850685168526853685468556856685768586859686068616862686368646865686668676868686968706871687268736874687568766877687868796880688168826883688468856886688768886889689068916892689368946895689668976898689969006901690269036904690569066907690869096910691169126913691469156916691769186919692069216922692369246925692669276928692969306931693269336934693569366937693869396940694169426943694469456946694769486949695069516952695369546955695669576958695969606961696269636964696569666967696869696970697169726973697469756976697769786979698069816982698369846985698669876988698969906991699269936994699569966997699869997000700170027003700470057006700770087009701070117012701370147015701670177018701970207021702270237024702570267027702870297030703170327033703470357036703770387039704070417042704370447045704670477048704970507051705270537054705570567057705870597060706170627063706470657066706770687069707070717072707370747075707670777078707970807081708270837084708570867087708870897090709170927093709470957096709770987099710071017102710371047105710671077108710971107111711271137114711571167117711871197120712171227123712471257126712771287129713071317132713371347135713671377138713971407141714271437144714571467147714871497150715171527153715471557156715771587159716071617162716371647165716671677168716971707171717271737174717571767177717871797180718171827183718471857186718771887189719071917192719371947195719671977198719972007201720272037204720572067207720872097210721172127213721472157216721772187219722072217222722372247225722672277228722972307231723272337234723572367237723872397240724172427243724472457246724772487249725072517252725372547255725672577258725972607261726272637264726572667267726872697270727172727273727472757276727772787279728072817282728372847285728672877288728972907291729272937294729572967297729872997300730173027303730473057306730773087309731073117312731373147315731673177318731973207321732273237324732573267327732873297330733173327333733473357336733773387339734073417342734373447345734673477348734973507351735273537354735573567357735873597360736173627363736473657366736773687369737073717372737373747375737673777378737973807381738273837384738573867387738873897390739173927393739473957396739773987399740074017402740374047405740674077408740974107411741274137414741574167417741874197420742174227423742474257426742774287429743074317432743374347435743674377438743974407441744274437444744574467447744874497450745174527453745474557456745774587459746074617462746374647465746674677468746974707471747274737474747574767477747874797480748174827483748474857486748774887489749074917492749374947495749674977498749975007501750275037504750575067507750875097510751175127513751475157516751775187519752075217522752375247525752675277528752975307531753275337534753575367537753875397540754175427543754475457546754775487549755075517552755375547555755675577558755975607561756275637564756575667567756875697570757175727573757475757576757775787579758075817582758375847585758675877588758975907591759275937594759575967597759875997600760176027603760476057606760776087609761076117612761376147615761676177618761976207621762276237624762576267627762876297630763176327633763476357636763776387639764076417642764376447645764676477648764976507651765276537654765576567657765876597660766176627663766476657666766776687669767076717672767376747675767676777678767976807681768276837684768576867687768876897690769176927693769476957696769776987699770077017702770377047705770677077708770977107711771277137714771577167717771877197720772177227723772477257726772777287729773077317732773377347735773677377738773977407741774277437744774577467747774877497750775177527753775477557756775777587759776077617762776377647765776677677768776977707771777277737774777577767777777877797780778177827783778477857786778777887789779077917792779377947795779677977798779978007801780278037804780578067807780878097810781178127813781478157816781778187819782078217822782378247825782678277828782978307831783278337834783578367837783878397840784178427843784478457846784778487849785078517852785378547855785678577858785978607861786278637864786578667867786878697870787178727873787478757876787778787879788078817882788378847885788678877888788978907891789278937894789578967897789878997900790179027903790479057906790779087909791079117912791379147915791679177918791979207921792279237924792579267927792879297930793179327933793479357936793779387939794079417942794379447945794679477948794979507951795279537954795579567957795879597960796179627963796479657966796779687969797079717972797379747975797679777978797979807981798279837984798579867987798879897990799179927993799479957996799779987999800080018002800380048005800680078008800980108011801280138014801580168017801880198020802180228023802480258026802780288029803080318032803380348035803680378038803980408041804280438044804580468047804880498050805180528053805480558056805780588059806080618062806380648065806680678068806980708071807280738074807580768077807880798080808180828083808480858086808780888089809080918092809380948095809680978098809981008101810281038104810581068107810881098110811181128113811481158116811781188119812081218122812381248125812681278128812981308131813281338134813581368137813881398140814181428143814481458146814781488149815081518152815381548155815681578158815981608161816281638164816581668167816881698170817181728173817481758176817781788179818081818182818381848185818681878188818981908191819281938194819581968197819881998200820182028203820482058206820782088209821082118212821382148215821682178218821982208221822282238224822582268227822882298230823182328233823482358236823782388239824082418242824382448245824682478248824982508251825282538254825582568257825882598260826182628263826482658266826782688269827082718272827382748275827682778278827982808281828282838284828582868287828882898290829182928293829482958296829782988299830083018302830383048305830683078308830983108311831283138314831583168317831883198320832183228323832483258326832783288329833083318332833383348335833683378338833983408341834283438344834583468347834883498350835183528353835483558356835783588359836083618362836383648365836683678368836983708371837283738374837583768377837883798380838183828383838483858386838783888389839083918392839383948395839683978398839984008401840284038404840584068407840884098410841184128413841484158416841784188419842084218422842384248425842684278428842984308431843284338434843584368437843884398440844184428443844484458446844784488449845084518452845384548455845684578458845984608461846284638464846584668467846884698470847184728473847484758476847784788479848084818482848384848485848684878488848984908491849284938494849584968497849884998500850185028503850485058506850785088509851085118512851385148515851685178518851985208521852285238524852585268527852885298530853185328533853485358536853785388539854085418542854385448545854685478548854985508551855285538554855585568557855885598560856185628563856485658566856785688569857085718572857385748575857685778578857985808581858285838584858585868587858885898590859185928593859485958596859785988599860086018602860386048605860686078608860986108611861286138614861586168617861886198620862186228623862486258626862786288629863086318632863386348635863686378638863986408641864286438644864586468647864886498650865186528653865486558656865786588659866086618662866386648665866686678668866986708671867286738674867586768677867886798680868186828683868486858686868786888689869086918692869386948695869686978698869987008701870287038704870587068707870887098710871187128713871487158716871787188719872087218722872387248725872687278728872987308731873287338734873587368737873887398740874187428743874487458746874787488749875087518752875387548755875687578758875987608761876287638764876587668767876887698770877187728773877487758776877787788779878087818782878387848785878687878788878987908791879287938794879587968797879887998800880188028803880488058806880788088809881088118812881388148815881688178818881988208821882288238824882588268827882888298830883188328833883488358836883788388839884088418842884388448845884688478848884988508851885288538854885588568857885888598860886188628863886488658866886788688869887088718872887388748875887688778878887988808881888288838884888588868887888888898890889188928893889488958896889788988899890089018902890389048905890689078908890989108911891289138914891589168917891889198920892189228923892489258926892789288929893089318932893389348935893689378938893989408941894289438944894589468947894889498950895189528953895489558956895789588959896089618962896389648965896689678968896989708971897289738974897589768977897889798980898189828983898489858986898789888989899089918992899389948995899689978998899990009001900290039004900590069007900890099010901190129013901490159016901790189019902090219022902390249025902690279028902990309031903290339034903590369037903890399040904190429043904490459046904790489049905090519052905390549055905690579058905990609061906290639064906590669067906890699070907190729073907490759076907790789079908090819082908390849085908690879088908990909091909290939094909590969097909890999100910191029103910491059106910791089109911091119112911391149115911691179118911991209121912291239124912591269127912891299130913191329133913491359136913791389139914091419142914391449145914691479148914991509151915291539154915591569157915891599160916191629163916491659166916791689169917091719172917391749175917691779178917991809181918291839184918591869187918891899190919191929193919491959196919791989199920092019202920392049205920692079208920992109211921292139214921592169217921892199220922192229223922492259226922792289229923092319232923392349235923692379238923992409241924292439244924592469247924892499250925192529253925492559256925792589259926092619262926392649265926692679268926992709271927292739274927592769277927892799280928192829283928492859286928792889289929092919292929392949295929692979298929993009301930293039304930593069307930893099310931193129313931493159316931793189319932093219322932393249325932693279328932993309331933293339334933593369337933893399340934193429343934493459346934793489349935093519352935393549355935693579358935993609361936293639364936593669367936893699370937193729373937493759376937793789379938093819382938393849385938693879388938993909391939293939394939593969397939893999400940194029403940494059406940794089409941094119412941394149415941694179418941994209421942294239424942594269427942894299430943194329433943494359436943794389439944094419442944394449445944694479448944994509451945294539454945594569457945894599460946194629463946494659466946794689469947094719472947394749475947694779478947994809481948294839484948594869487948894899490949194929493949494959496949794989499950095019502950395049505950695079508950995109511951295139514951595169517951895199520952195229523952495259526952795289529953095319532953395349535953695379538953995409541954295439544954595469547954895499550955195529553955495559556955795589559956095619562956395649565956695679568956995709571957295739574957595769577957895799580958195829583958495859586958795889589959095919592959395949595959695979598959996009601960296039604960596069607960896099610961196129613961496159616961796189619962096219622962396249625962696279628962996309631963296339634963596369637963896399640964196429643964496459646964796489649965096519652965396549655965696579658965996609661966296639664966596669667966896699670967196729673967496759676967796789679968096819682968396849685968696879688968996909691969296939694969596969697969896999700970197029703970497059706970797089709971097119712971397149715971697179718971997209721972297239724972597269727972897299730973197329733973497359736973797389739974097419742974397449745974697479748974997509751975297539754975597569757975897599760976197629763976497659766976797689769977097719772977397749775977697779778977997809781978297839784978597869787978897899790979197929793979497959796979797989799980098019802980398049805980698079808980998109811981298139814981598169817981898199820982198229823982498259826982798289829983098319832983398349835983698379838983998409841984298439844984598469847984898499850985198529853985498559856985798589859986098619862986398649865986698679868986998709871987298739874987598769877987898799880988198829883988498859886988798889889989098919892989398949895989698979898989999009901990299039904990599069907990899099910991199129913991499159916991799189919992099219922992399249925992699279928992999309931993299339934993599369937993899399940994199429943994499459946994799489949995099519952995399549955995699579958995999609961996299639964996599669967996899699970997199729973997499759976997799789979998099819982998399849985998699879988998999909991999299939994999599969997999899991000010001100021000310004100051000610007100081000910010100111001210013100141001510016100171001810019100201002110022100231002410025100261002710028100291003010031100321003310034100351003610037100381003910040100411004210043100441004510046100471004810049100501005110052100531005410055100561005710058100591006010061100621006310064100651006610067100681006910070100711007210073100741007510076100771007810079100801008110082100831008410085100861008710088100891009010091100921009310094100951009610097100981009910100101011010210103101041010510106101071010810109101101011110112101131011410115101161011710118101191012010121101221012310124101251012610127101281012910130101311013210133101341013510136101371013810139101401014110142101431014410145101461014710148101491015010151101521015310154101551015610157101581015910160101611016210163101641016510166101671016810169101701017110172101731017410175101761017710178101791018010181101821018310184101851018610187101881018910190101911019210193101941019510196101971019810199102001020110202102031020410205102061020710208102091021010211102121021310214102151021610217102181021910220102211022210223102241022510226102271022810229102301023110232102331023410235102361023710238102391024010241102421024310244102451024610247102481024910250102511025210253102541025510256102571025810259102601026110262102631026410265102661026710268102691027010271102721027310274102751027610277102781027910280102811028210283102841028510286102871028810289102901029110292102931029410295102961029710298102991030010301103021030310304103051030610307103081030910310103111031210313103141031510316103171031810319103201032110322103231032410325103261032710328103291033010331103321033310334103351033610337103381033910340103411034210343103441034510346103471034810349103501035110352103531035410355103561035710358103591036010361103621036310364103651036610367103681036910370103711037210373103741037510376103771037810379103801038110382103831038410385103861038710388103891039010391103921039310394103951039610397103981039910400104011040210403104041040510406104071040810409104101041110412104131041410415104161041710418104191042010421104221042310424104251042610427104281042910430104311043210433104341043510436104371043810439104401044110442104431044410445104461044710448104491045010451104521045310454104551045610457104581045910460104611046210463104641046510466104671046810469104701047110472104731047410475104761047710478104791048010481104821048310484104851048610487104881048910490104911049210493104941049510496104971049810499105001050110502105031050410505105061050710508105091051010511105121051310514105151051610517105181051910520105211052210523105241052510526105271052810529105301053110532105331053410535105361053710538105391054010541105421054310544105451054610547105481054910550105511055210553105541055510556105571055810559105601056110562105631056410565105661056710568105691057010571105721057310574105751057610577105781057910580105811058210583105841058510586105871058810589105901059110592105931059410595105961059710598105991060010601106021060310604106051060610607106081060910610106111061210613106141061510616106171061810619106201062110622106231062410625106261062710628106291063010631106321063310634106351063610637106381063910640106411064210643106441064510646106471064810649106501065110652106531065410655106561065710658106591066010661106621066310664106651066610667106681066910670106711067210673106741067510676106771067810679106801068110682106831068410685106861068710688106891069010691106921069310694106951069610697106981069910700107011070210703107041070510706107071070810709107101071110712107131071410715107161071710718107191072010721107221072310724107251072610727107281072910730107311073210733107341073510736107371073810739107401074110742107431074410745107461074710748107491075010751107521075310754107551075610757107581075910760107611076210763107641076510766107671076810769107701077110772107731077410775107761077710778107791078010781107821078310784107851078610787107881078910790107911079210793107941079510796107971079810799108001080110802108031080410805108061080710808108091081010811108121081310814108151081610817108181081910820108211082210823108241082510826108271082810829108301083110832108331083410835108361083710838108391084010841108421084310844108451084610847108481084910850108511085210853108541085510856108571085810859108601086110862108631086410865108661086710868108691087010871108721087310874108751087610877108781087910880108811088210883108841088510886108871088810889108901089110892108931089410895108961089710898108991090010901109021090310904109051090610907109081090910910109111091210913109141091510916109171091810919109201092110922109231092410925109261092710928109291093010931109321093310934109351093610937109381093910940109411094210943109441094510946109471094810949109501095110952109531095410955109561095710958109591096010961109621096310964109651096610967109681096910970109711097210973109741097510976109771097810979109801098110982109831098410985109861098710988109891099010991109921099310994109951099610997109981099911000110011100211003110041100511006110071100811009110101101111012110131101411015110161101711018110191102011021110221102311024110251102611027110281102911030110311103211033110341103511036110371103811039110401104111042110431104411045110461104711048110491105011051110521105311054110551105611057110581105911060110611106211063110641106511066110671106811069110701107111072110731107411075110761107711078110791108011081110821108311084110851108611087110881108911090110911109211093110941109511096110971109811099111001110111102111031110411105111061110711108111091111011111111121111311114111151111611117111181111911120111211112211123111241112511126111271112811129111301113111132111331113411135111361113711138111391114011141111421114311144111451114611147111481114911150111511115211153111541115511156111571115811159111601116111162111631116411165111661116711168111691117011171111721117311174111751117611177111781117911180111811118211183111841118511186111871118811189111901119111192111931119411195111961119711198111991120011201112021120311204112051120611207112081120911210112111121211213112141121511216112171121811219112201122111222112231122411225112261122711228112291123011231112321123311234112351123611237112381123911240112411124211243112441124511246112471124811249112501125111252112531125411255112561125711258112591126011261112621126311264112651126611267112681126911270112711127211273112741127511276112771127811279112801128111282112831128411285112861128711288112891129011291112921129311294112951129611297112981129911300113011130211303113041130511306113071130811309113101131111312113131131411315113161131711318113191132011321113221132311324113251132611327113281132911330113311133211333113341133511336113371133811339113401134111342113431134411345113461134711348113491135011351113521135311354113551135611357113581135911360113611136211363113641136511366113671136811369113701137111372113731137411375113761137711378113791138011381113821138311384113851138611387113881138911390113911139211393113941139511396113971139811399114001140111402114031140411405114061140711408114091141011411114121141311414114151141611417114181141911420114211142211423114241142511426114271142811429114301143111432114331143411435114361143711438114391144011441114421144311444114451144611447114481144911450114511145211453114541145511456114571145811459114601146111462114631146411465114661146711468114691147011471114721147311474114751147611477114781147911480114811148211483114841148511486114871148811489114901149111492114931149411495114961149711498114991150011501115021150311504115051150611507115081150911510115111151211513115141151511516115171151811519115201152111522115231152411525115261152711528115291153011531115321153311534115351153611537115381153911540115411154211543115441154511546115471154811549115501155111552115531155411555115561155711558115591156011561115621156311564115651156611567115681156911570115711157211573115741157511576115771157811579115801158111582115831158411585115861158711588115891159011591115921159311594115951159611597115981159911600116011160211603116041160511606116071160811609116101161111612116131161411615116161161711618116191162011621116221162311624116251162611627116281162911630116311163211633116341163511636116371163811639116401164111642116431164411645116461164711648116491165011651116521165311654116551165611657116581165911660116611166211663116641166511666116671166811669116701167111672116731167411675116761167711678116791168011681116821168311684116851168611687116881168911690116911169211693116941169511696116971169811699117001170111702117031170411705117061170711708117091171011711117121171311714117151171611717117181171911720117211172211723117241172511726117271172811729117301173111732117331173411735117361173711738117391174011741117421174311744117451174611747117481174911750117511175211753117541175511756117571175811759117601176111762117631176411765117661176711768117691177011771117721177311774117751177611777117781177911780117811178211783117841178511786117871178811789117901179111792117931179411795117961179711798117991180011801118021180311804118051180611807118081180911810118111181211813118141181511816118171181811819118201182111822118231182411825118261182711828118291183011831118321183311834118351183611837118381183911840118411184211843118441184511846118471184811849118501185111852118531185411855118561185711858118591186011861118621186311864118651186611867118681186911870118711187211873118741187511876118771187811879118801188111882118831188411885118861188711888118891189011891118921189311894118951189611897118981189911900119011190211903119041190511906119071190811909119101191111912119131191411915119161191711918119191192011921119221192311924119251192611927119281192911930119311193211933119341193511936119371193811939119401194111942119431194411945119461194711948119491195011951119521195311954119551195611957119581195911960119611196211963119641196511966119671196811969119701197111972119731197411975119761197711978119791198011981119821198311984119851198611987119881198911990119911199211993119941199511996119971199811999120001200112002120031200412005120061200712008120091201012011120121201312014120151201612017120181201912020120211202212023120241202512026120271202812029120301203112032120331203412035120361203712038120391204012041120421204312044120451204612047120481204912050120511205212053120541205512056120571205812059120601206112062120631206412065120661206712068120691207012071120721207312074120751207612077120781207912080120811208212083120841208512086120871208812089120901209112092120931209412095120961209712098120991210012101121021210312104121051210612107121081210912110121111211212113121141211512116121171211812119121201212112122121231212412125121261212712128121291213012131121321213312134121351213612137121381213912140121411214212143121441214512146121471214812149121501215112152121531215412155121561215712158121591216012161121621216312164121651216612167121681216912170121711217212173121741217512176121771217812179121801218112182121831218412185121861218712188121891219012191121921219312194121951219612197121981219912200122011220212203122041220512206122071220812209122101221112212122131221412215122161221712218122191222012221122221222312224122251222612227122281222912230122311223212233122341223512236122371223812239122401224112242122431224412245122461224712248122491225012251122521225312254122551225612257122581225912260122611226212263122641226512266122671226812269122701227112272122731227412275122761227712278122791228012281122821228312284122851228612287122881228912290122911229212293122941229512296122971229812299123001230112302123031230412305123061230712308123091231012311123121231312314123151231612317123181231912320123211232212323123241232512326123271232812329123301233112332123331233412335123361233712338123391234012341123421234312344123451234612347123481234912350123511235212353123541235512356123571235812359123601236112362123631236412365123661236712368123691237012371123721237312374123751237612377123781237912380123811238212383123841238512386123871238812389123901239112392123931239412395123961239712398123991240012401124021240312404124051240612407124081240912410124111241212413124141241512416124171241812419124201242112422124231242412425124261242712428124291243012431124321243312434124351243612437124381243912440124411244212443124441244512446124471244812449124501245112452124531245412455124561245712458124591246012461124621246312464124651246612467124681246912470124711247212473124741247512476124771247812479124801248112482124831248412485124861248712488124891249012491124921249312494124951249612497124981249912500125011250212503125041250512506125071250812509125101251112512125131251412515125161251712518125191252012521125221252312524125251252612527125281252912530125311253212533125341253512536125371253812539125401254112542125431254412545125461254712548125491255012551125521255312554125551255612557125581255912560125611256212563125641256512566125671256812569125701257112572125731257412575125761257712578125791258012581125821258312584125851258612587125881258912590125911259212593125941259512596125971259812599126001260112602126031260412605126061260712608126091261012611126121261312614126151261612617126181261912620126211262212623126241262512626126271262812629126301263112632126331263412635126361263712638126391264012641126421264312644126451264612647126481264912650126511265212653126541265512656126571265812659126601266112662126631266412665126661266712668126691267012671126721267312674126751267612677126781267912680126811268212683126841268512686126871268812689126901269112692126931269412695126961269712698126991270012701127021270312704127051270612707127081270912710127111271212713127141271512716127171271812719127201272112722127231272412725127261272712728127291273012731127321273312734127351273612737127381273912740127411274212743127441274512746127471274812749127501275112752127531275412755127561275712758127591276012761127621276312764127651276612767127681276912770127711277212773127741277512776127771277812779127801278112782127831278412785127861278712788127891279012791127921279312794127951279612797127981279912800128011280212803128041280512806128071280812809128101281112812128131281412815128161281712818128191282012821128221282312824128251282612827128281282912830128311283212833128341283512836128371283812839128401284112842128431284412845128461284712848128491285012851128521285312854128551285612857128581285912860128611286212863128641286512866128671286812869128701287112872128731287412875128761287712878128791288012881128821288312884128851288612887128881288912890128911289212893128941289512896128971289812899129001290112902129031290412905129061290712908129091291012911129121291312914129151291612917129181291912920129211292212923129241292512926129271292812929129301293112932129331293412935129361293712938129391294012941129421294312944129451294612947129481294912950129511295212953129541295512956129571295812959129601296112962129631296412965129661296712968129691297012971129721297312974129751297612977129781297912980129811298212983129841298512986129871298812989129901299112992129931299412995129961299712998129991300013001130021300313004130051300613007130081300913010130111301213013130141301513016130171301813019130201302113022130231302413025130261302713028130291303013031130321303313034130351303613037130381303913040130411304213043130441304513046130471304813049130501305113052130531305413055130561305713058130591306013061130621306313064130651306613067130681306913070130711307213073130741307513076130771307813079130801308113082130831308413085130861308713088130891309013091130921309313094130951309613097130981309913100131011310213103131041310513106131071310813109131101311113112131131311413115131161311713118131191312013121131221312313124131251312613127131281312913130131311313213133131341313513136131371313813139131401314113142131431314413145131461314713148131491315013151131521315313154131551315613157131581315913160131611316213163131641316513166131671316813169131701317113172131731317413175131761317713178131791318013181131821318313184131851318613187131881318913190131911319213193131941319513196131971319813199132001320113202132031320413205132061320713208132091321013211132121321313214132151321613217132181321913220132211322213223132241322513226132271322813229132301323113232132331323413235132361323713238132391324013241132421324313244132451324613247132481324913250132511325213253132541325513256132571325813259132601326113262132631326413265132661326713268132691327013271132721327313274132751327613277132781327913280132811328213283132841328513286132871328813289132901329113292132931329413295132961329713298132991330013301133021330313304133051330613307133081330913310133111331213313133141331513316133171331813319133201332113322133231332413325133261332713328133291333013331133321333313334133351333613337133381333913340133411334213343133441334513346133471334813349133501335113352133531335413355133561335713358133591336013361133621336313364133651336613367133681336913370133711337213373133741337513376133771337813379133801338113382133831338413385133861338713388133891339013391133921339313394133951339613397133981339913400134011340213403134041340513406134071340813409134101341113412134131341413415134161341713418134191342013421134221342313424134251342613427134281342913430134311343213433134341343513436134371343813439134401344113442134431344413445134461344713448134491345013451134521345313454134551345613457134581345913460134611346213463134641346513466134671346813469134701347113472134731347413475134761347713478134791348013481134821348313484134851348613487134881348913490134911349213493134941349513496134971349813499135001350113502135031350413505135061350713508135091351013511135121351313514135151351613517135181351913520135211352213523135241352513526135271352813529135301353113532135331353413535135361353713538135391354013541135421354313544135451354613547135481354913550135511355213553135541355513556135571355813559135601356113562135631356413565135661356713568135691357013571135721357313574135751357613577135781357913580135811358213583135841358513586135871358813589135901359113592135931359413595135961359713598135991360013601136021360313604136051360613607136081360913610136111361213613136141361513616136171361813619136201362113622136231362413625136261362713628136291363013631136321363313634136351363613637136381363913640136411364213643136441364513646136471364813649136501365113652136531365413655136561365713658136591366013661136621366313664136651366613667136681366913670136711367213673136741367513676136771367813679136801368113682136831368413685136861368713688136891369013691136921369313694136951369613697136981369913700137011370213703137041370513706137071370813709137101371113712137131371413715137161371713718137191372013721137221372313724137251372613727137281372913730137311373213733137341373513736137371373813739137401374113742137431374413745137461374713748137491375013751137521375313754137551375613757137581375913760137611376213763137641376513766137671376813769137701377113772137731377413775137761377713778137791378013781137821378313784137851378613787137881378913790137911379213793137941379513796137971379813799138001380113802138031380413805138061380713808138091381013811138121381313814138151381613817138181381913820138211382213823138241382513826138271382813829138301383113832138331383413835138361383713838138391384013841138421384313844138451384613847138481384913850138511385213853138541385513856138571385813859138601386113862138631386413865138661386713868138691387013871138721387313874138751387613877138781387913880138811388213883138841388513886138871388813889138901389113892138931389413895138961389713898138991390013901139021390313904139051390613907139081390913910139111391213913139141391513916139171391813919139201392113922139231392413925139261392713928139291393013931139321393313934139351393613937139381393913940139411394213943139441394513946139471394813949139501395113952139531395413955139561395713958139591396013961139621396313964139651396613967139681396913970139711397213973139741397513976139771397813979139801398113982139831398413985139861398713988139891399013991139921399313994139951399613997139981399914000140011400214003140041400514006140071400814009140101401114012140131401414015140161401714018140191402014021140221402314024140251402614027140281402914030140311403214033140341403514036140371403814039140401404114042140431404414045140461404714048140491405014051140521405314054140551405614057140581405914060140611406214063140641406514066140671406814069140701407114072140731407414075140761407714078140791408014081140821408314084140851408614087140881408914090140911409214093140941409514096140971409814099141001410114102141031410414105141061410714108141091411014111141121411314114141151411614117141181411914120141211412214123141241412514126141271412814129141301413114132141331413414135141361413714138141391414014141141421414314144141451414614147141481414914150141511415214153141541415514156141571415814159141601416114162141631416414165141661416714168141691417014171141721417314174141751417614177141781417914180141811418214183141841418514186141871418814189141901419114192141931419414195141961419714198141991420014201142021420314204142051420614207142081420914210142111421214213142141421514216142171421814219142201422114222142231422414225142261422714228142291423014231142321423314234142351423614237142381423914240142411424214243142441424514246142471424814249142501425114252142531425414255142561425714258142591426014261142621426314264142651426614267142681426914270142711427214273142741427514276142771427814279142801428114282142831428414285142861428714288142891429014291142921429314294142951429614297142981429914300143011430214303143041430514306143071430814309143101431114312143131431414315143161431714318143191432014321143221432314324143251432614327143281432914330143311433214333143341433514336143371433814339143401434114342143431434414345143461434714348143491435014351143521435314354143551435614357143581435914360143611436214363143641436514366143671436814369143701437114372143731437414375143761437714378143791438014381143821438314384143851438614387143881438914390143911439214393143941439514396143971439814399144001440114402144031440414405144061440714408144091441014411144121441314414144151441614417144181441914420144211442214423144241442514426144271442814429144301443114432144331443414435144361443714438144391444014441144421444314444144451444614447144481444914450144511445214453144541445514456144571445814459144601446114462144631446414465144661446714468144691447014471144721447314474144751447614477144781447914480144811448214483144841448514486144871448814489144901449114492144931449414495144961449714498144991450014501145021450314504145051450614507145081450914510145111451214513145141451514516145171451814519145201452114522145231452414525145261452714528145291453014531145321453314534145351453614537145381453914540145411454214543145441454514546145471454814549145501455114552145531455414555145561455714558145591456014561145621456314564145651456614567145681456914570145711457214573145741457514576145771457814579145801458114582145831458414585145861458714588145891459014591145921459314594145951459614597145981459914600146011460214603146041460514606146071460814609146101461114612146131461414615146161461714618146191462014621146221462314624146251462614627146281462914630146311463214633146341463514636146371463814639146401464114642146431464414645146461464714648146491465014651146521465314654146551465614657146581465914660146611466214663146641466514666146671466814669146701467114672146731467414675146761467714678146791468014681146821468314684146851468614687146881468914690146911469214693146941469514696146971469814699147001470114702147031470414705147061470714708147091471014711147121471314714147151471614717147181471914720147211472214723147241472514726147271472814729147301473114732147331473414735147361473714738147391474014741147421474314744147451474614747147481474914750147511475214753147541475514756147571475814759147601476114762147631476414765147661476714768147691477014771147721477314774147751477614777147781477914780147811478214783147841478514786147871478814789147901479114792147931479414795147961479714798147991480014801148021480314804148051480614807148081480914810148111481214813148141481514816148171481814819148201482114822148231482414825148261482714828148291483014831148321483314834148351483614837148381483914840148411484214843148441484514846148471484814849148501485114852148531485414855148561485714858148591486014861148621486314864148651486614867148681486914870148711487214873148741487514876148771487814879148801488114882148831488414885148861488714888148891489014891148921489314894148951489614897148981489914900149011490214903149041490514906149071490814909149101491114912149131491414915149161491714918149191492014921149221492314924149251492614927149281492914930149311493214933149341493514936149371493814939149401494114942149431494414945149461494714948149491495014951149521495314954149551495614957149581495914960149611496214963149641496514966149671496814969149701497114972149731497414975149761497714978149791498014981149821498314984149851498614987149881498914990149911499214993149941499514996149971499814999150001500115002150031500415005150061500715008150091501015011150121501315014 |
- #!/usr/bin/perl -T
- #------------------------------------------------------------------------------
- # This is amavisd-new.
- # It is an interface between message transfer agent (MTA) and virus
- # scanners and/or spam scanners, functioning as a mail content filter.
- #
- # It is a performance-enhanced and feature-enriched version of amavisd
- # (which in turn is a daemonized version of AMaViS), initially based
- # on amavisd-snapshot-20020300).
- #
- # All work since amavisd-snapshot-20020300:
- # Copyright (C) 2002,2003,2004,2005 Mark Martinec, All Rights Reserved.
- # with contributions from the amavis-* mailing lists and individuals,
- # as acknowledged in the release notes.
- #
- # This program is free software; you can redistribute it and/or modify
- # it under the terms of the GNU General Public License as published by
- # the Free Software Foundation; either version 2 of the License, or
- # (at your option) any later version.
- #
- # This program is distributed in the hope that it will be useful,
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- # GNU General Public License for details.
- #
- # You should have received a copy of the GNU General Public License
- # along with this program; if not, write to the Free Software
- # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
- # Author: Mark Martinec <mark.martinec@ijs.si>
- # Patches and problem reports are welcome.
- #
- # The latest version of this program is available at:
- # http://www.ijs.si/software/amavisd/
- #------------------------------------------------------------------------------
- # Here is a boilerplate from the amavisd(-snapshot) version,
- # which is the version that served as a base code for the initial
- # version of amavisd-new. License terms were the same:
- #
- # Author: Chris Mason <cmason@unixzone.com>
- # Current maintainer: Lars Hecking <lhecking@users.sourceforge.net>
- # Based on work by:
- # Mogens Kjaer, Carlsberg Laboratory, <mk@crc.dk>
- # Juergen Quade, Softing GmbH, <quade@softing.com>
- # Christian Bricart <shiva@aachalon.de>
- # Rainer Link <link@foo.fh-furtwangen.de>
- # This script is part of the AMaViS package. For more information see:
- # http://amavis.org/
- # Copyright (C) 2000 - 2002 the people mentioned above
- # This software is licensed under the GNU General Public License (GPL)
- # See: http://www.gnu.org/copyleft/gpl.html
- #------------------------------------------------------------------------------
- #------------------------------------------------------------------------------
- #Index of packages in this file
- # Amavis::Boot
- # Amavis::Conf
- # Amavis::Lock
- # Amavis::Log
- # Amavis::Timing
- # Amavis::Util
- # Amavis::rfc2821_2822_Tools
- # Amavis::Lookup::RE
- # Amavis::Lookup::IP
- # Amavis::Lookup::Label
- # Amavis::Lookup
- # Amavis::Expand
- # Amavis::IO::Zlib
- # Amavis::In::Connection
- # Amavis::In::Message::PerRecip
- # Amavis::In::Message
- # Amavis::Out::EditHeader
- # Amavis::Out::Local
- # Amavis::Out
- # Amavis::UnmangleSender
- # Amavis::Unpackers::NewFilename
- # Amavis::Unpackers::Part
- # Amavis::Unpackers::OurFiler
- # Amavis::Unpackers::Validity
- # Amavis::Unpackers::MIME
- # Amavis::Notify
- # Amavis::Cache
- # Amavis
- #optionally compiled-in packages: ---------------------------------------------
- # Amavis::DB::SNMP
- # Amavis::DB
- # Amavis::Cache
- # Amavis::Out::SQL::Connection
- # Amavis::Out::SQL::Log
- # Amavis::IO::SQL
- # Amavis::Out::SQL::Quarantine
- # Amavis::Lookup::SQLfield
- # Amavis::Lookup::SQL
- # Amavis::LDAP::Connection
- # Amavis::Lookup::LDAP
- # Amavis::Lookup::LDAPattr
- # Amavis::In::AMCL
- # Amavis::In::SMTP
- # Amavis::AV
- # Amavis::SpamControl
- # Amavis::Unpackers
- #------------------------------------------------------------------------------
- #
- package Amavis::Boot;
- use strict;
- use re 'taint';
- # Fetch all required modules (or nicely report missing ones), and compile them
- # once-and-for-all at the parent process, so that forked children can inherit
- # and share already compiled code in memory. Children will still need to 'use'
- # modules if they want to inherit from their name space.
- #
- sub fetch_modules($$@) {
- my($reason, $required, @modules) = @_;
- my(@missing);
- for my $m (@modules) {
- local($_) = $m;
- $_ .= /^auto::/ ? '.al' : '.pm' if !/\.(pm|pl|al)\z/;
- s[::][/]g;
- eval { require $_ } or push(@missing, $m);
- }
- die "ERROR: MISSING $reason:\n" . join('', map { " $_\n" } @missing)
- if $required && @missing;
- \@missing;
- }
- BEGIN {
- fetch_modules('REQUIRED BASIC MODULES', 1, qw(
- Exporter POSIX Fcntl Socket Errno Carp Time::HiRes
- IO::Handle IO::File IO::Socket IO::Socket::UNIX IO::Socket::INET
- IO::Wrap IO::Stringy Digest::MD5 Unix::Syslog File::Basename
- Mail::Field Mail::Address Mail::Header Mail::Internet Compress::Zlib
- MIME::Base64 MIME::QuotedPrint MIME::Words
- MIME::Head MIME::Body MIME::Entity MIME::Parser MIME::Decoder
- MIME::Decoder::Base64 MIME::Decoder::Binary MIME::Decoder::QuotedPrint
- MIME::Decoder::NBit MIME::Decoder::UU MIME::Decoder::Gzip64
- Net::Cmd Net::SMTP Net::Server Net::Server::PreForkSimple
- ));
- # with earlier versions of Perl one may need to add additional modules
- # to the list, such as: auto::POSIX::setgid auto::POSIX::setuid ...
- fetch_modules('OPTIONAL BASIC MODULES', 0, qw(
- Carp::Heavy auto::POSIX::setgid auto::POSIX::setuid
- MIME::Decoder::BinHex
- ));
- }
- 1;
- #
- package Amavis::Conf;
- use strict;
- use re 'taint';
- # prototypes
- sub D_REJECT();
- sub D_BOUNCE();
- sub D_DISCARD();
- sub D_PASS();
- BEGIN {
- use Exporter ();
- use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.043';
- @ISA = qw(Exporter);
- %EXPORT_TAGS = (
- 'dynamic_confvars' => [qw(
- $policy_bank_name $protocol @inet_acl
- $log_level $log_templ $log_recip_templ $forward_method $notify_method
- $amavis_auth_user $amavis_auth_pass $auth_reauthenticate_forwarded
- $auth_required_out $auth_required_inp $auth_required_release
- @auth_mech_avail
- $local_client_bind_address
- $localhost_name $smtpd_greeting_banner $smtpd_quit_banner
- $smtpd_message_size_limit
- $final_virus_destiny $final_spam_destiny
- $final_banned_destiny $final_bad_header_destiny
- $warnvirussender $warnspamsender $warnbannedsender $warnbadhsender
- $warn_offsite
- @av_scanners @av_scanners_backup $first_infected_stops_scan
- $bypass_decode_parts @decoders
- $defang_virus $defang_banned $defang_spam
- $defang_bad_header $defang_undecipherable $defang_all
- $undecipherable_subject_tag
- $sa_spam_report_header $sa_spam_level_char
- $sa_mail_body_size_limit
- $localpart_is_case_sensitive
- $recipient_delimiter $replace_existing_extension
- $hdr_encoding $bdy_encoding $hdr_encoding_qb
- $notify_xmailer_header $X_HEADER_TAG $X_HEADER_LINE
- $remove_existing_x_scanned_headers $remove_existing_spam_headers
- $hdrfrom_notify_sender $hdrfrom_notify_recip
- $hdrfrom_notify_admin $hdrfrom_notify_spamadmin
- $mailfrom_notify_sender $mailfrom_notify_recip
- $mailfrom_notify_admin $mailfrom_notify_spamadmin
- $mailfrom_to_quarantine
- $virus_quarantine_method $spam_quarantine_method
- $banned_files_quarantine_method $bad_header_quarantine_method
- %local_delivery_aliases
- $notify_sender_templ
- $notify_virus_sender_templ $notify_spam_sender_templ
- $notify_virus_admin_templ $notify_spam_admin_templ
- $notify_virus_recips_templ $notify_spam_recips_templ
- $banned_namepath_re
- $per_recip_whitelist_sender_lookup_tables
- $per_recip_blacklist_sender_lookup_tables
- %sql_clause
- @local_domains_maps @mynetworks_maps
- @bypass_virus_checks_maps @bypass_spam_checks_maps
- @bypass_banned_checks_maps @bypass_header_checks_maps
- @virus_lovers_maps @spam_lovers_maps
- @banned_files_lovers_maps @bad_header_lovers_maps
- @warnvirusrecip_maps @warnbannedrecip_maps @warnbadhrecip_maps
- @newvirus_admin_maps @virus_admin_maps
- @banned_admin_maps @bad_header_admin_maps @spam_admin_maps
- @virus_quarantine_to_maps
- @banned_quarantine_to_maps @bad_header_quarantine_to_maps
- @spam_quarantine_to_maps @spam_quarantine_bysender_to_maps
- @banned_filename_maps
- @spam_tag_level_maps @spam_tag2_level_maps @spam_kill_level_maps
- @spam_dsn_cutoff_level_maps @spam_quarantine_cutoff_level_maps
- @spam_modifies_subj_maps @spam_subject_tag_maps @spam_subject_tag2_maps
- @whitelist_sender_maps @blacklist_sender_maps @score_sender_maps
- @message_size_limit_maps
- @addr_extension_virus_maps @addr_extension_spam_maps
- @addr_extension_banned_maps @addr_extension_bad_header_maps
- @debug_sender_maps %recipient_policy_bank_map %recipient_policy_bank_re_map $sa_site_rules_filename
- )],
- 'confvars' => [qw(
- $myproduct_name $myversion_id $myversion_id_numeric $myversion_date
- $myversion $myhostname
- $MYHOME $TEMPBASE $QUARANTINEDIR $quarantine_subdir_levels
- $daemonize $pid_file $lock_file $db_home
- $enable_db $enable_global_cache
- $daemon_user $daemon_group $daemon_chroot_dir $path
- $DEBUG $DO_SYSLOG $SYSLOG_LEVEL $LOGFILE
- $max_servers $max_requests $child_timeout
- %current_policy_bank %policy_bank %interface_policy
- $unix_socketname $inet_socket_port $inet_socket_bind
- $insert_received_line $relayhost_is_client $smtpd_recipient_limit
- $MAXLEVELS $MAXFILES
- $MIN_EXPANSION_QUOTA $MIN_EXPANSION_FACTOR
- $MAX_EXPANSION_QUOTA $MAX_EXPANSION_FACTOR
- @lookup_sql_dsn @storage_sql_dsn
- $virus_check_negative_ttl $virus_check_positive_ttl
- $spam_check_negative_ttl $spam_check_positive_ttl
- $enable_ldap $default_ldap
- @keep_decoded_original_maps @map_full_type_to_short_type_maps
- @viruses_that_fake_sender_maps %banned_rules
- $file %recipient_policy_bank_map %recipient_policy_bank_re_map $sa_site_rules_filename
- )],
- 'sa' => [qw(
- $helpers_home $dspam
- $sa_local_tests_only $sa_auto_whitelist $sa_timeout $sa_debug
- $sa_site_rules_filename
- )],
- 'platform' => [qw(
- $can_truncate $unicode_aware $eol
- &D_REJECT &D_BOUNCE &D_DISCARD &D_PASS
- )],
- # other variables settable by user in amavisd.conf,
- # but not directly accessible by the program
- 'hidden_confvars' => [qw(
- $mydomain
- )],
- # legacy variables, predeclared for compatibility of amavisd.conf
- # The rest of the program does not use them directly and they should not be
- # visible in other modules, but may be referenced through @*_maps variables
- 'legacy_confvars' => [qw(
- %local_domains @local_domains_acl $local_domains_re @mynetworks
- %bypass_virus_checks @bypass_virus_checks_acl $bypass_virus_checks_re
- %bypass_spam_checks @bypass_spam_checks_acl $bypass_spam_checks_re
- %bypass_banned_checks @bypass_banned_checks_acl $bypass_banned_checks_re
- %bypass_header_checks @bypass_header_checks_acl $bypass_header_checks_re
- %virus_lovers @virus_lovers_acl $virus_lovers_re
- %spam_lovers @spam_lovers_acl $spam_lovers_re
- %banned_files_lovers @banned_files_lovers_acl $banned_files_lovers_re
- %bad_header_lovers @bad_header_lovers_acl $bad_header_lovers_re
- %virus_admin %spam_admin
- $newvirus_admin $virus_admin $banned_admin $bad_header_admin $spam_admin
- $warnvirusrecip $warnbannedrecip $warnbadhrecip
- $virus_quarantine_to $banned_quarantine_to $bad_header_quarantine_to
- $spam_quarantine_to $spam_quarantine_bysender_to
- $keep_decoded_original_re $map_full_type_to_short_type_re
- $banned_filename_re $viruses_that_fake_sender_re
- $sa_tag_level_deflt $sa_tag2_level_deflt $sa_kill_level_deflt
- $sa_dsn_cutoff_level $sa_quarantine_cutoff_level
- $sa_spam_modifies_subj $sa_spam_subject_tag1 $sa_spam_subject_tag
- %whitelist_sender @whitelist_sender_acl $whitelist_sender_re
- %blacklist_sender @blacklist_sender_acl $blacklist_sender_re
- $addr_extension_virus $addr_extension_spam
- $addr_extension_banned $addr_extension_bad_header
- $sql_select_policy $sql_select_white_black_list
- $gets_addr_in_quoted_form @debug_sender_acl
- $arc $bzip2 $lzop $lha $unarj $gzip $uncompress $unfreeze
- $unrar $zoo $pax $cpio $ar $rpm2cpio $cabextract $ripole $tnef
- $gunzip $bunzip2 $unlzop
- )],
- );
- Exporter::export_tags qw(dynamic_confvars confvars sa platform
- hidden_confvars legacy_confvars);
- } # BEGIN
- use POSIX ();
- use Carp ();
- use Errno qw(ENOENT EACCES);
- use vars @EXPORT;
- sub c($); sub cr($); sub ca($); # prototypes
- use subs qw(c cr ca); # access subroutine to new-style config variables
- BEGIN { push(@EXPORT,qw(c cr ca)) }
- { # initialize policy bank hash containing dynamic config settings
- for my $tag (@EXPORT_TAGS{'dynamic_confvars'}) {
- for my $v (@$tag) {
- if ($v !~ /^([%\$\@])(.*)\z/) { die "Unsupported variable type: $v" }
- else {
- no strict 'refs'; my($type,$name) = ($1,$2);
- $current_policy_bank{$name} = $type eq '$' ? \${"Amavis::Conf::$name"}
- : $type eq '@' ? \@{"Amavis::Conf::$name"}
- : $type eq '%' ? \%{"Amavis::Conf::$name"}
- : undef;
- }
- }
- }
- $current_policy_bank{'policy_bank_name'} = ''; # builtin policy
- $current_policy_bank{'policy_bank_path'} = '';
- $policy_bank{''} = { %current_policy_bank }; # copy
- }
- # new-style access to dynamic config variables
- # return a config variable value - usually a scalar;
- # one level of indirection for scalars is allowed
- sub c($) {
- my($name) = @_;
- if (!exists $current_policy_bank{$name}) {
- Carp::croak(sprintf('No entry "%s" in policy bank "%s"',
- $name, $current_policy_bank{'policy_bank_name'}));
- }
- my($var) = $current_policy_bank{$name}; my($r) = ref($var);
- !$r ? $var : $r eq 'SCALAR' ? $$var
- : $r eq 'ARRAY' ? @$var : $r eq 'HASH' ? %$var : $var;
- }
- # return a ref to a config variable value, or undef if var is undefined
- sub cr($) {
- my($name) = @_;
- if (!exists $current_policy_bank{$name}) {
- Carp::croak(sprintf('No entry "%s" in policy bank "%s"',
- $name, $current_policy_bank{'policy_bank_name'}));
- }
- my($var) = $current_policy_bank{$name};
- !defined($var) ? undef : !ref($var) ? \$var : $var;
- }
- # return a ref to a config variable value (which is supposed to be an array),
- # converting undef to an empty array, and a scalar to a one-element array
- # if necessary
- sub ca($) {
- my($name) = @_;
- if (!exists $current_policy_bank{$name}) {
- Carp::croak(sprintf('No entry "%s" in policy bank "%s"',
- $name, $current_policy_bank{'policy_bank_name'}));
- }
- my($var) = $current_policy_bank{$name};
- !defined($var) ? [] : !ref($var) ? [$var] : $var;
- }
- $myproduct_name = 'amavisd-new';
- $myversion_id = '2.3.3'; $myversion_date = '20050822';
- $myversion = "$myproduct_name-$myversion_id ($myversion_date)";
- $myversion_id_numeric = # x.yyyzzz, allows numerical comparision, like Perl $]
- sprintf("%8.6f", $1 + ($2 + $3/1000)/1000)
- if $myversion_id =~ /^(\d+)(?:\.(\d*)(?:\.(\d*))?)?(.*)$/;
- $eol = "\n"; # native record separator in files: LF or CRLF or even CR
- $unicode_aware = $]>=5.008 && length("\x{263a}")==1 && eval { require Encode };
- # serves only as a quick default for other configuration settings
- $MYHOME = '/var/amavis';
- $mydomain = '!change-mydomain-variable!.example.com';#intentionally bad default
- # Create debugging output - true: log to stderr; false: log to syslog/file
- $DEBUG = 0;
- # Cause Net::Server parameters 'background' and 'setsid' to be set,
- # resulting in the program to detach itself from the terminal
- $daemonize = 1;
- # Net::Server pre-forking settings - defaults, overruled by amavisd.conf
- $max_servers = 2; # number of pre-forked children
- $max_requests = 10; # retire a child after that many accepts
- $child_timeout = 8*60; # abort child if it does not complete each task in n sec
- # Can file be truncated?
- # Set to 1 if 'truncate' works (it is XPG4-UNIX standard feature,
- # not required by Posix).
- # Things will go faster with SMTP-in, otherwise (e.g. with milter)
- # it makes no difference as file truncation will not be used.
- $can_truncate = 1;
- # expiration time of cached results: time to live in seconds
- # (how long the result of a virus/spam test remains valid)
- $virus_check_negative_ttl= 3*60; # time to remember that mail was not infected
- $virus_check_positive_ttl= 30*60; # time to remember that mail was infected
- $spam_check_negative_ttl = 30*60; # time to remember that mail was not spam
- $spam_check_positive_ttl = 30*60; # time to remember that mail was spam
- #
- # NOTE:
- # Cache size will be determined by the largest of the $*_ttl values.
- # Depending on the mail rate, the cache database may grow quite large.
- # Reasonable compromise for the max value is 15 minutes to 2 hours.
- # Customizable notification messages, logging
- $SYSLOG_LEVEL = 'mail.debug';
- $enable_db = 0; # load optional modules Amavis::DB & Amavis::DB::SNMP
- $enable_global_cache = 0; # enable use of bdb-based Amavis::Cache
- # Where to find SQL server(s) and database to support SQL lookups?
- # A list of triples: (dsn,user,passw). Specify more than one
- # for multiple (backup) SQL servers.
- #
- #@storage_sql_dsn =
- #@lookup_sql_dsn =
- # ( ['DBI:mysql:mail:host1', 'some-username1', 'some-password1'],
- # ['DBI:mysql:mail:host2', 'some-username2', 'some-password2'] );
- # The SQL select clause to fetch per-recipient policy settings
- # The %k will be replaced by a comma-separated list of query addresses
- # (e.g. full address, domain only, catchall). Use ORDER, if there
- # is a chance that multiple records will match - the first match wins
- # If field names are not unique (e.g. 'id'), the later field overwrites the
- # earlier in a hash returned by lookup, which is why we use '*,users.id'.
- $sql_select_policy =
- 'SELECT *,users.id FROM users LEFT JOIN policy ON users.policy_id=policy.id'.
- ' WHERE users.email IN (%k) ORDER BY users.priority DESC';
- # The SQL select clause to check sender in per-recipient whitelist/blacklist
- # The first SELECT argument '?' will be users.id from recipient SQL lookup,
- # the %k will be sender addresses (e.g. full address, domain only, catchall).
- # Only the first occurrence of '?' will be replaced by users.id, subsequent
- # occurrences of '?' will see empty string as an argument. There can be zero
- # or more occurrences of %k, lookup keys will be multiplied accordingly.
- # Up until version 2.2.0 the '?' had to be placed before the '%k';
- # starting with 2.2.1 this restriction is lifted.
- $sql_select_white_black_list =
- 'SELECT wb FROM wblist LEFT JOIN mailaddr ON wblist.sid=mailaddr.id'.
- ' WHERE (wblist.rid=?) AND (mailaddr.email IN (%k))'.
- ' ORDER BY mailaddr.priority DESC';
- %sql_clause = (
- 'sel_policy' => \$sql_select_policy,
- 'sel_wblist' => \$sql_select_white_black_list,
- 'sel_adr' =>
- 'SELECT id FROM maddr WHERE email=?',
- 'ins_adr' =>
- 'INSERT INTO maddr (email, domain) VALUES (?,?)',
- 'ins_msg' =>
- 'INSERT INTO msgs (mail_id, secret_id, am_id, time_num, time_iso, sid,'.
- ' policy, client_addr, size, host) VALUES (?,?,?,?,?,?,?,?,?,?)',
- 'upd_msg' =>
- 'UPDATE msgs SET content=?, quar_type=?, dsn_sent=?, spam_level=?,'.
- ' message_id=?, from_addr=?, subject=? WHERE mail_id=?',
- 'ins_rcp' =>
- 'INSERT INTO msgrcpt (mail_id, rid, ds, rs, bl, wl, bspam_level,'.
- ' smtp_resp) VALUES (?,?,?,?,?,?,?,?)',
- 'ins_quar' =>
- 'INSERT INTO quarantine (mail_id, chunk_ind, mail_text) VALUES (?,?,?)',
- 'sel_quar' =>
- 'SELECT mail_text FROM quarantine WHERE mail_id=? ORDER BY chunk_ind',
- );
- #
- # Receiving mail related
- # $unix_socketname = '/var/amavis/amavisd.sock'; # traditional amavis client protocol
- # $inet_socket_port = 10024; # accept SMTP on this TCP port
- # $inet_socket_port = [10024,10026,10027]; # ...possibly on more than one
- $inet_socket_bind = '127.0.0.1'; # limit socket bind to loopback interface
- @inet_acl = qw( 127.0.0.1 [::1] ); # allow SMTP access only from localhost
- @mynetworks = qw( 127.0.0.0/8 [::1] [FE80::]/10 [FEC0::]/10
- 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 );
- $notify_method = 'smtp:[127.0.0.1]:10025';
- $forward_method = 'smtp:[127.0.0.1]:10025';
- #old defaults:
- # $virus_quarantine_method = 'local:virus-%i-%n';
- # $spam_quarantine_method = 'local:spam-%b-%i-%n.gz';
- # $banned_files_quarantine_method = 'local:banned-%i-%n';
- # $bad_header_quarantine_method = 'local:badh-%i-%n';
- #new defaults:
- $virus_quarantine_method = 'local:virus-%m';
- $spam_quarantine_method = 'local:spam-%m.gz';
- $banned_files_quarantine_method = 'local:banned-%m';
- $bad_header_quarantine_method = 'local:badh-%m';
- $insert_received_line = 1; # insert 'Received:' header field? (not with milter)
- $remove_existing_x_scanned_headers = 0;
- $remove_existing_spam_headers = 1;
- # encoding (charset in MIME terminology)
- # to be used in RFC 2047-encoded ...
- $hdr_encoding = 'iso-8859-1'; # ... header field bodies
- $bdy_encoding = 'iso-8859-1'; # ... notification body text
- # encoding (encoding in MIME terminology)
- $hdr_encoding_qb = 'Q'; # quoted-printable (default)
- #$hdr_encoding_qb = 'B'; # base64 (usual for far east charsets)
- $smtpd_recipient_limit = 1100; # max recipients (RCPT TO) - sanity limit
- # $myhostname is used by SMTP server module in the initial SMTP welcome line,
- # in inserted 'Received:' lines, Message-ID in notifications, log entries, ...
- $myhostname = (POSIX::uname)[1]; # should be a FQDN !
- $smtpd_greeting_banner = '${helo-name} ${protocol} ${product} service ready';
- $smtpd_quit_banner = '${helo-name} ${product} closing transmission channel';
- # $localhost_name is the name of THIS host running amavisd
- # (typically 'localhost'). It is used in HELO SMTP command
- # when reinjecting mail back to MTA via SMTP for final delivery.
- $localhost_name = 'localhost';
- # @auth_mech_avail = ('PLAIN','LOGIN'); # empty list disables incoming AUTH
- #$auth_required_inp = 1; # incoming SMTP authentication required by amavisd?
- #$auth_required_out = 1; # SMTP authentication required by MTA
- $auth_required_release = 1; # secret_id is required for a quarantine release
- # SMTP AUTH username and password for notification submissions
- # (and reauthentication of forwarded mail if requested)
- #$amavis_auth_user = undef; # perhaps: 'amavisd'
- #$amavis_auth_pass = undef;
- #$auth_reauthenticate_forwarded = undef; # supply our own credentials also
- # for forwarded (passed) mail
- # whom quarantined messages appear to be sent from (envelope sender)
- # $mailfrom_to_quarantine = undef; # original sender if undef, or set explicitly
- # where to send quarantined malware
- # Specify undef to disable, or e-mail address containing '@',
- # or just a local part, which will be mapped by %local_delivery_aliases
- # into local mailbox name or directory. The lookup key is a recipient address
- $virus_quarantine_to = 'virus-quarantine'; # %local_delivery_aliases mapped
- $banned_quarantine_to = 'banned-quarantine'; # %local_delivery_aliases mapped
- $bad_header_quarantine_to = 'bad-header-quarantine'; # %local_delivery_aliases
- $spam_quarantine_to = 'spam-quarantine'; # %local_delivery_aliases mapped
- $banned_admin = \@virus_admin_maps; # compatibility
- $bad_header_admin = \@virus_admin_maps; # compatibility
- # similar to $spam_quarantine_to, but the lookup key is the sender address
- $spam_quarantine_bysender_to = undef; # dflt: no by-sender spam quarantine
- # quarantine directory or mailbox file or empty
- # (only used if $virus_quarantine_to specifies direct local delivery)
- $QUARANTINEDIR = undef; # no quarantine unless overridden by config
- $undecipherable_subject_tag = '***UNCHECKED*** ';
- # string to prepend to Subject header field when message qualifies as spam
- # $sa_spam_subject_tag1 = undef; # example: '***possible SPAM*** '
- # $sa_spam_subject_tag = undef; # example: '***SPAM*** '
- $sa_spam_modifies_subj = 1; # true for compatibility; can be a
- # lookup table indicating per-recip settings
- $sa_spam_level_char = '*'; # character to be used in X-Spam-Level bar;
- # empty or undef disables adding this header field
- # $sa_spam_report_header = undef; # insert X-Spam-Report header field?
- $sa_local_tests_only = 0;
- $sa_debug = undef;
- $sa_timeout = 30; # timeout in seconds for a call to SpamAssassin
- # MIME defanging is only done when enabled and malware is allowed to pass
- # $defang_virus = undef;
- # $defang_banned = undef;
- # $defang_spam = undef;
- # $defang_bad_header = undef;
- # $defang_undecipherable = undef;
- # $defang_all = undef;
- $file = 'file'; # path to the file(1) utility for classifying contents
- $MIN_EXPANSION_FACTOR = 5; # times original mail size
- $MAX_EXPANSION_FACTOR = 500; # times original mail size
- # See amavisd.conf and README.lookups for details.
- # What to do with the message (this is independent of quarantining):
- # Reject: tell MTA to generate a non-delivery notification, MTA gets 5xx
- # Bounce: generate a non-delivery notification by ourselves, MTA gets 250
- # Discard: drop the message and pretend it was delivered, MTA gets 250
- # Pass: deliver/accept the message
- #
- # Bounce and Reject are similar: in both cases sender gets a non-delivery
- # notification, either generated by amavisd-new, or by MTA. The notification
- # issued by amavisd-new may be more informative, while on the other hand
- # MTA may be able to do a true reject on the original SMTP session
- # (e.g. with sendmail milter), or else it just generates normal non-delivery
- # notification / bounce (e.g. with Postfix, Exim). As a consequence,
- # with Postfix and Exim and dual-sendmail setup the Bounce is more informative
- # than Reject, but sendmail-milter users may prefer Reject.
- #
- # Bounce and Discard are similar: in both cases amavisd-new confirms
- # to MTA the message reception with success code 250. The difference is
- # in sender notification: Bounce sends a non-delivery notification to sender,
- # Discard does not, the message is silently dropped. Quarantine and
- # admin notifications are not affected by any of these settings.
- #
- # COMPATIBITITY NOTE: the separation of *_destiny values into
- # D_BOUNCE, D_REJECT, D_DISCARD and D_PASS made settings $warnvirussender
- # and $warnspamsender only still useful with D_PASS. The combination of
- # D_DISCARD + $warn*sender=1 is mapped into D_BOUNCE for compatibility.
- # intentionally leave value -1 unassigned for compatibility
- sub D_REJECT () { -3 }
- sub D_BOUNCE () { -2 }
- sub D_DISCARD() { 0 }
- sub D_PASS () { 1 }
- # The following symbolic constants can be used in *destiny settings:
- #
- # D_PASS mail will pass to recipients, regardless of contents;
- #
- # D_DISCARD mail will not be delivered to its recipients, sender will NOT be
- # notified. Effectively we lose mail (but it will be quarantined
- # unless disabled).
- #
- # D_BOUNCE mail will not be delivered to its recipients, a non-delivery
- # notification (bounce) will be sent to the sender by amavisd-new;
- # Exception: bounce (DSN) will not be sent if a virus name matches
- # $viruses_that_fake_sender_maps, or to messages from mailing lists
- # (Precedence: bulk|list|junk), or for spam exceeding
- # spam_dsn_cutoff_level
- #
- # D_REJECT mail will not be delivered to its recipients, sender should
- # preferably get a reject, e.g. SMTP permanent reject response
- # (e.g. with milter), or non-delivery notification from MTA
- # (e.g. Postfix). If this is not possible (e.g. different recipients
- # have different tolerances to bad mail contents and not using LMTP)
- # amavisd-new sends a bounce by itself (same as D_BOUNCE).
- #
- # Notes:
- # D_REJECT and D_BOUNCE are similar, the difference is in who is responsible
- # for informing the sender about non-delivery, and how informative
- # the notification can be (amavisd-new knows more than MTA);
- # With D_REJECT, MTA may reject original SMTP, or send DSN (delivery status
- # notification, colloquially called 'bounce') - depending on MTA;
- # Best suited for sendmail milter, especially for spam.
- # With D_BOUNCE, amavisd-new (not MTA) sends DSN (can better explain the
- # reason for mail non-delivery but unable to reject the original
- # SMTP session, and is in position to suppress DSN if considered
- # unsuitable). Best suited for Postfix and other dual-MTA setups.
- $final_virus_destiny = D_DISCARD; # D_REJECT, D_BOUNCE, D_DISCARD, D_PASS
- $final_banned_destiny = D_BOUNCE; # D_REJECT, D_BOUNCE, D_DISCARD, D_PASS
- $final_spam_destiny = D_BOUNCE; # D_REJECT, D_BOUNCE, D_DISCARD, D_PASS
- $final_bad_header_destiny = D_PASS; # D_REJECT, D_BOUNCE, D_DISCARD, D_PASS
- # If you decide to pass viruses (or spam) to certain users using
- # @virus_lovers_maps, (or @spam_lovers_maps), or $final_virus_destiny=D_PASS
- # ($final_spam_destiny=D_PASS), you can set the variable $addr_extension_virus
- # ($addr_extension_spam) to some string, and the recipient address will have
- # this string appended as an address extension to the local-part of the
- # address. This extension can be used by final local delivery agent to place
- # such mail in different folders. Leave these variables undefined or empty
- # strings to prevent appending address extensions. Setting has no effect
- # on users which will not be receiving viruses (spam). Recipients which
- # do not match access lists in @local_domains_maps are not affected (i.e.
- # non-local recipients do not get address extension appended).
- #
- # LDAs usually default to stripping away address extension if no special
- # handling for it is specified, so having this option enabled normally
- # does no harm, provided the $recipients_delimiter character matches
- # the setting at the final MTA's local delivery agent (LDA).
- #
- # $addr_extension_virus = 'virus'; # for example
- # $addr_extension_spam = 'spam';
- # $addr_extension_banned = 'banned';
- # $addr_extension_bad_header = 'badh';
- # Delimiter between local part of the recipient address and address extension
- # (which can optionally be added, see variables $addr_extension_virus and
- # $addr_extension_spam). E.g. recipient address <user@domain.example> gets
- # changed to <user+virus@domain.example>.
- #
- # Delimiter should match equivalent (final) MTA delimiter setting.
- # (e.g. for Postfix add 'recipient_delimiter = +' to main.cf).
- # Setting it to an empty string or to undef disables this feature
- # regardless of $addr_extension_virus and $addr_extension_spam settings.
- # $recipient_delimiter = '+';
- $replace_existing_extension = 1; # true: replace ext; false: append ext
- # Affects matching of localpart of e-mail addresses (left of '@')
- # in lookups: true = case sensitive, false = case insensitive
- $localpart_is_case_sensitive = 0;
- # first match wins, more specific entries should precede general ones!
- # the result may be a string or a ref to a list of strings;
- # see also sub decompose_part()
- $map_full_type_to_short_type_re = Amavis::Lookup::RE->new(
- [qr/^empty\z/ => 'empty'],
- [qr/^directory\z/ => 'dir'],
- [qr/^can't (stat|read)\b/ => 'dat'], # file(1) diagnostics
- [qr/^cannot open\b/ => 'dat'], # file(1) diagnostics
- [qr/^ERROR: Corrupted\b/ => 'dat'], # file(1) diagnostics
- [qr/can't read magic file|couldn't find any magic files/ => 'dat'],
- [qr/^data\z/ => 'dat'],
- [qr/^ISO-8859.*\btext\b/ => 'txt'],
- [qr/^Non-ISO.*ASCII\b.*\btext\b/ => 'txt'],
- [qr/^Unicode\b.*\btext\b/i => 'txt'],
- [qr/^'diff' output text\b/ => 'txt'],
- [qr/^GNU message catalog\b/ => 'mo'],
- [qr/^PGP encrypted data\b/ => 'pgp'],
- [qr/^PGP armored data( signed)? message\b/ => ['pgp','pgp.asc'] ],
- [qr/^PGP armored\b/ => ['pgp','pgp.asc'] ],
- ### 'file' is a bit too trigger happy to claim something is 'mail text'
- # [qr/^RFC 822 mail text\b/ => 'mail'],
- [qr/^(ASCII|smtp|RFC 822) mail text\b/ => 'txt'],
- [qr/^JPEG image data\b/ =>['image','jpg'] ],
- [qr/^GIF image data\b/ =>['image','gif'] ],
- [qr/^PNG image data\b/ =>['image','png'] ],
- [qr/^TIFF image data\b/ =>['image','tif'] ],
- [qr/^PCX\b.*\bimage data\b/ =>['image','pcx'] ],
- [qr/^PC bitmap data\b/ =>['image','bmp'] ],
- [qr/^MP2\b/ =>['audio','mpa','mp2'] ],
- [qr/^MP3\b/ =>['audio','mpa','mp3'] ],
- [qr/^MPEG video stream data\b/ =>['movie','mpv'] ],
- [qr/^MPEG system stream data\b/ =>['movie','mpg'] ],
- [qr/^MPEG\b/ =>['movie','mpg'] ],
- [qr/^Microsoft ASF\b/ =>['movie','wmv'] ],
- [qr/^RIFF\b.*\bAVI\b/ =>['movie','avi'] ],
- [qr/^RIFF\b.*\bWAVE audio\b/ =>['audio','wav'] ],
- [qr/^Macromedia Flash data\b/ => 'swf'],
- [qr/^HTML document text\b/ => 'html'],
- [qr/^XML document text\b/ => 'xml'],
- [qr/^exported SGML document text\b/ => 'sgml'],
- [qr/^PostScript document text\b/ => 'ps'],
- [qr/^PDF document\b/ => 'pdf'],
- [qr/^Rich Text Format data\b/ => 'rtf'],
- [qr/^Microsoft Office Document\b/i => 'doc'], # OLE2: doc, ppt, xls, ...
- [qr/^LaTeX\b.*\bdocument text\b/ => 'lat'],
- [qr/^TeX DVI file\b/ => 'dvi'],
- [qr/\bdocument text\b/ => 'txt'],
- [qr/^compiled Java class data\b/ => 'java'],
- [qr/^MS Windows 95 Internet shortcut text\b/ => 'url'],
- [qr/^frozen\b/ => 'F'],
- [qr/^gzip compressed\b/ => 'gz'],
- [qr/^bzip compressed\b/ => 'bz'],
- [qr/^bzip2 compressed\b/ => 'bz2'],
- [qr/^lzop compressed\b/ => 'lzo'],
- [qr/^compress'd/ => 'Z'],
- [qr/^Zip archive\b/i => 'zip'],
- [qr/^RAR archive\b/i => 'rar'],
- [qr/^LHa.*\barchive\b/i => 'lha'], # (also known as .lzh)
- [qr/^ARC archive\b/i => 'arc'],
- [qr/^ARJ archive\b/i => 'arj'],
- [qr/^Zoo archive\b/i => 'zoo'],
- [qr/^(\S+\s+)?tar archive\b/i => 'tar'],
- [qr/^(\S+\s+)?cpio archive\b/i => 'cpio'],
- [qr/^Debian binary package\b/i => 'deb'], # standard Unix archive (ar)
- [qr/^current ar archive\b/i => 'a'], # standard Unix archive (ar)
- [qr/^RPM\b/ => 'rpm'],
- [qr/^(Transport Neutral Encapsulation Format|TNEF)\b/i => 'tnef'],
- [qr/^Microsoft cabinet file\b/ => 'cab'],
- [qr/^(uuencoded|xxencoded)\b/i => 'uue'],
- [qr/^binhex\b/i => 'hqx'],
- [qr/^(ASCII|text)\b/i => 'asc'],
- [qr/^Emacs.*byte-compiled Lisp data/i => 'asc'], # BinHex with an empty line
- [qr/\bscript text executable\b/ => 'txt'],
- [qr/^MS-DOS\b.*\bexecutable\b/ => ['exe','exe-ms'] ],
- [qr/^MS Windows\b.*\bexecutable\b/ => ['exe','exe-ms'] ],
- [qr/^PA-RISC.*\bexecutable\b/ => ['exe','exe-unix'] ],
- [qr/^ELF .*\bexecutable\b/ => ['exe','exe-unix'] ],
- [qr/^COFF format .*\bexecutable\b/ => ['exe','exe-unix'] ],
- [qr/^executable \(RISC System\b/ => ['exe','exe-unix'] ],
- [qr/^VMS\b.*\bexecutable\b/ => ['exe','exe-vms'] ],
- [qr/\bexecutable\b/i => 'exe'],
- [qr/^MS Windows\b.*\bDLL\b/ => 'dll'],
- [qr/\bshared object, /i => 'so'],
- [qr/\brelocatable, /i => 'o'],
- [qr/\btext\b/i => 'asc'],
- [qr/^/ => 'dat'], # catchall
- );
- # MS Windows PE 32-bit Intel 80386 GUI executable not relocatable
- # MS-DOS executable (EXE), OS/2 or MS Windows
- # PA-RISC1.1 executable dynamically linked
- # PA-RISC1.1 shared executable dynamically linked
- # ELF 64-bit LSB executable, Alpha (unofficial), version 1 (FreeBSD), for FreeBSD 5.0.1, dynamically linked (uses shared libs), stripped
- # ELF 64-bit LSB executable, Alpha (unofficial), version 1 (SYSV), for GNU/Linux 2.2.5, dynamically linked (uses shared libs), stripped
- # ELF 64-bit MSB executable, SPARC V9, version 1 (FreeBSD), for FreeBSD 5.0, dynamically linked (uses shared libs), stripped
- # ELF 64-bit MSB shared object, SPARC V9, version 1 (FreeBSD), stripped
- # ELF 32-bit LSB executable, Intel 80386, version 1, dynamically`
- # ELF 32-bit MSB executable, SPARC, version 1, dynamically linke`
- # COFF format alpha executable paged stripped - version 3.11-10
- # COFF format alpha executable paged dynamically linked stripped`
- # COFF format alpha demand paged executable or object module stripped - version 3.11-10
- # COFF format alpha paged dynamically linked not stripped shared`
- # executable (RISC System/6000 V3.1) or obj module
- # VMS VAX executable
- # prototypes
- sub Amavis::Unpackers::do_mime_decode($$);
- sub Amavis::Unpackers::do_ascii($$);
- sub Amavis::Unpackers::do_uncompress($$$);
- sub Amavis::Unpackers::do_gunzip($$);
- sub Amavis::Unpackers::do_pax_cpio($$$);
- sub Amavis::Unpackers::do_tar($$);
- sub Amavis::Unpackers::do_ar($$$);
- sub Amavis::Unpackers::do_unzip($$);
- sub Amavis::Unpackers::do_unrar($$$);
- sub Amavis::Unpackers::do_unarj($$$);
- sub Amavis::Unpackers::do_arc($$$);
- sub Amavis::Unpackers::do_zoo($$$);
- sub Amavis::Unpackers::do_lha($$$);
- sub Amavis::Unpackers::do_ole($$$);
- sub Amavis::Unpackers::do_cabextract($$$);
- sub Amavis::Unpackers::do_tnef($$);
- sub Amavis::Unpackers::do_tnef_ext($$$);
- sub Amavis::Unpackers::do_executable($$@);
- # Define alias names or shortcuts in this module to make it simpler
- # to call these routines from amavisd.conf
- *read_text = \&Amavis::Util::read_text;
- *read_l10n_templates = \&Amavis::Util::read_l10n_templates;
- *read_hash = \&Amavis::Util::read_hash;
- *read_array = \&Amavis::Util::read_array;
- *dump_hash = \&Amavis::Util::dump_hash;
- *dump_array = \&Amavis::Util::dump_array;
- *ask_daemon = \&Amavis::AV::ask_daemon;
- *sophos_savi = \&Amavis::AV::ask_sophos_savi;
- *ask_clamav = \&Amavis::AV::ask_clamav;
- *do_mime_decode = \&Amavis::Unpackers::do_mime_decode;
- *do_ascii = \&Amavis::Unpackers::do_ascii;
- *do_uncompress = \&Amavis::Unpackers::do_uncompress;
- *do_gunzip = \&Amavis::Unpackers::do_gunzip;
- *do_pax_cpio = \&Amavis::Unpackers::do_pax_cpio;
- *do_tar = \&Amavis::Unpackers::do_tar;
- *do_ar = \&Amavis::Unpackers::do_ar;
- *do_unzip = \&Amavis::Unpackers::do_unzip;
- *do_unrar = \&Amavis::Unpackers::do_unrar;
- *do_unarj = \&Amavis::Unpackers::do_unarj;
- *do_arc = \&Amavis::Unpackers::do_arc;
- *do_zoo = \&Amavis::Unpackers::do_zoo;
- *do_lha = \&Amavis::Unpackers::do_lha;
- *do_ole = \&Amavis::Unpackers::do_ole;
- *do_cabextract = \&Amavis::Unpackers::do_cabextract;
- *do_tnef_ext = \&Amavis::Unpackers::do_tnef_ext;
- *do_tnef = \&Amavis::Unpackers::do_tnef;
- *do_executable = \&Amavis::Unpackers::do_executable;
- sub new_RE { Amavis::Lookup::RE->new(@_) }
- # initialize the @decoders list
- sub init_decoders() {
- # A list of pairs or n-tuples: [short-type, code_ref, optional-args...].
- # Maps short types to a decoding routine, the first match wins.
- # Arguments beyond the first two can be program path string (or a listref of
- # paths to be searched) or a reference to a variable containing such a path,
- # which allows for lazy evaluation, making possible to assign values to
- # legacy configuration variables even after the assignment to @decoders.
- @decoders = (
- ['mail', \&Amavis::Unpackers::do_mime_decode],
- ['asc', \&Amavis::Unpackers::do_ascii],
- ['uue', \&Amavis::Unpackers::do_ascii],
- ['hqx', \&Amavis::Unpackers::do_ascii],
- ['ync', \&Amavis::Unpackers::do_ascii],
- ['F', \&Amavis::Unpackers::do_uncompress, \$unfreeze],
- ['Z', \&Amavis::Unpackers::do_uncompress, \$uncompress],
- ['gz', \&Amavis::Unpackers::do_gunzip],
- ['gz', \&Amavis::Unpackers::do_uncompress, \$gunzip],
- ['bz2', \&Amavis::Unpackers::do_uncompress, \$bunzip2],
- ['lzo', \&Amavis::Unpackers::do_uncompress, \$unlzop],
- ['rpm', \&Amavis::Unpackers::do_uncompress, \$rpm2cpio],
- ['cpio', \&Amavis::Unpackers::do_pax_cpio, \$pax],
- ['cpio', \&Amavis::Unpackers::do_pax_cpio, \$cpio],
- ['tar', \&Amavis::Unpackers::do_pax_cpio, \$pax],
- ['tar', \&Amavis::Unpackers::do_pax_cpio, \$cpio],
- ['tar', \&Amavis::Unpackers::do_tar],
- ['deb', \&Amavis::Unpackers::do_ar, \$ar],
- # ['a', \&Amavis::Unpackers::do_ar, \$ar], #unpacking .a seems an overkill
- ['zip', \&Amavis::Unpackers::do_unzip],
- ['rar', \&Amavis::Unpackers::do_unrar, \$unrar],
- ['arj', \&Amavis::Unpackers::do_unarj, \$unarj],
- ['arc', \&Amavis::Unpackers::do_arc, \$arc],
- ['zoo', \&Amavis::Unpackers::do_zoo, \$zoo],
- ['lha', \&Amavis::Unpackers::do_lha, \$lha],
- ['doc', \&Amavis::Unpackers::do_ole, \$ripole],
- ['cab', \&Amavis::Unpackers::do_cabextract, \$cabextract],
- ['tnef', \&Amavis::Unpackers::do_tnef_ext, \$tnef],
- ['tnef', \&Amavis::Unpackers::do_tnef],
- ['exe', \&Amavis::Unpackers::do_executable, \$unrar,\$lha,\$unarj],
- );
- }
- sub build_default_maps() {
- @local_domains_maps = (
- \%local_domains, \@local_domains_acl, \$local_domains_re);
- @mynetworks_maps = (\@mynetworks);
- @bypass_virus_checks_maps = (
- \%bypass_virus_checks, \@bypass_virus_checks_acl, \$bypass_virus_checks_re);
- @bypass_spam_checks_maps = (
- \%bypass_spam_checks, \@bypass_spam_checks_acl, \$bypass_spam_checks_re);
- @bypass_banned_checks_maps = (
- \%bypass_banned_checks, \@bypass_banned_checks_acl, \$bypass_banned_checks_re);
- @bypass_header_checks_maps = (
- \%bypass_header_checks, \@bypass_header_checks_acl, \$bypass_header_checks_re);
- @virus_lovers_maps = (
- \%virus_lovers, \@virus_lovers_acl, \$virus_lovers_re);
- @spam_lovers_maps = (
- \%spam_lovers, \@spam_lovers_acl, \$spam_lovers_re);
- @banned_files_lovers_maps = (
- \%banned_files_lovers, \@banned_files_lovers_acl, \$banned_files_lovers_re);
- @bad_header_lovers_maps = (
- \%bad_header_lovers, \@bad_header_lovers_acl, \$bad_header_lovers_re);
- @warnvirusrecip_maps = (\$warnvirusrecip);
- @warnbannedrecip_maps = (\$warnbannedrecip);
- @warnbadhrecip_maps = (\$warnbadhrecip);
- @newvirus_admin_maps = (\$newvirus_admin);
- @virus_admin_maps = (\%virus_admin, \$virus_admin);
- @banned_admin_maps = (\$banned_admin);
- @bad_header_admin_maps= (\$bad_header_admin);
- @spam_admin_maps = (\%spam_admin, \$spam_admin);
- @virus_quarantine_to_maps = (\$virus_quarantine_to);
- @banned_quarantine_to_maps = (\$banned_quarantine_to);
- @bad_header_quarantine_to_maps = (\$bad_header_quarantine_to);
- @spam_quarantine_to_maps = (\$spam_quarantine_to);
- @spam_quarantine_bysender_to_maps = (\$spam_quarantine_bysender_to);
- @keep_decoded_original_maps = (\$keep_decoded_original_re);
- @map_full_type_to_short_type_maps = (\$map_full_type_to_short_type_re);
- # @banned_filename_maps = ( {'.' => [$banned_filename_re]} );
- # @banned_filename_maps = ( {'.' => 'DEFAULT'} );#names mapped by %banned_rules
- @banned_filename_maps = ( 'DEFAULT' ); # same as previous, but shorter
- @viruses_that_fake_sender_maps = (\$viruses_that_fake_sender_re, 1);
- @spam_tag_level_maps = (\$sa_tag_level_deflt);
- @spam_tag2_level_maps = (\$sa_tag2_level_deflt);
- @spam_kill_level_maps = (\$sa_kill_level_deflt);
- @spam_dsn_cutoff_level_maps = (\$sa_dsn_cutoff_level);
- @spam_quarantine_cutoff_level_maps = (\$sa_quarantine_cutoff_level);
- @spam_modifies_subj_maps = (\$sa_spam_modifies_subj);
- @spam_subject_tag_maps = (\$sa_spam_subject_tag1); # note: inconsistent
- @spam_subject_tag2_maps = (\$sa_spam_subject_tag); # note: inconsistent
- @whitelist_sender_maps = (
- \%whitelist_sender, \@whitelist_sender_acl, \$whitelist_sender_re);
- @blacklist_sender_maps = (
- \%blacklist_sender, \@blacklist_sender_acl, \$blacklist_sender_re);
- @score_sender_maps = (); # new variable, no backwards compatibility needed
- @message_size_limit_maps = (); # new variable
- @addr_extension_virus_maps = (\$addr_extension_virus);
- @addr_extension_spam_maps = (\$addr_extension_spam);
- @addr_extension_banned_maps = (\$addr_extension_banned);
- @addr_extension_bad_header_maps = (\$addr_extension_bad_header);
- @debug_sender_maps = (\@debug_sender_acl);
- }
- # prepend a lookup table label object for logging purposes
- sub label_default_maps() {
- for my $varname (qw(
- @local_domains_maps @mynetworks_maps
- @bypass_virus_checks_maps @bypass_spam_checks_maps
- @bypass_banned_checks_maps @bypass_header_checks_maps
- @virus_lovers_maps @spam_lovers_maps
- @banned_files_lovers_maps @bad_header_lovers_maps
- @warnvirusrecip_maps @warnbannedrecip_maps @warnbadhrecip_maps
- @newvirus_admin_maps @virus_admin_maps
- @banned_admin_maps @bad_header_admin_maps @spam_admin_maps
- @virus_quarantine_to_maps
- @banned_quarantine_to_maps @bad_header_quarantine_to_maps
- @spam_quarantine_to_maps @spam_quarantine_bysender_to_maps
- @keep_decoded_original_maps @map_full_type_to_short_type_maps
- @banned_filename_maps
- @viruses_that_fake_sender_maps
- @spam_tag_level_maps @spam_tag2_level_maps @spam_kill_level_maps
- @spam_dsn_cutoff_level_maps @spam_quarantine_cutoff_level_maps
- @spam_modifies_subj_maps @spam_subject_tag_maps @spam_subject_tag2_maps
- @whitelist_sender_maps @blacklist_sender_maps @score_sender_maps
- @message_size_limit_maps
- @addr_extension_virus_maps @addr_extension_spam_maps
- @addr_extension_banned_maps @addr_extension_bad_header_maps
- @debug_sender_maps ))
- {
- my($g) = $varname; $g =~ s{\@}{Amavis::Conf::}; # qualified variable name
- my($label) = $varname; $label=~s/^\@//; $label=~s/_maps$//;
- { no strict 'refs';
- unshift(@$g, # NOTE: a symbolic reference
- Amavis::Lookup::Label->new($label)) if @$g; # no label if empty
- }
- }
- }
- # read and evaluate configuration files (one or more)
- sub read_config(@) {
- my(@config_files) = @_;
- for my $config_file (@config_files) {
- my($msg);
- my($errn) = stat($config_file) ? 0 : 0+$!;
- if ($errn == ENOENT) { $msg = "does not exist" }
- elsif ($errn) { $msg = "is inaccessible: $!" }
- elsif (-d _) { $msg = "is a directory" }
- elsif (!-f _) { $msg = "is not a regular file" }
- elsif ($> && -o _) { $msg = "is owned by EUID $>, should be owned by root"}
- elsif ($> && -w _) { $msg = "is writable by EUID $>, EGID $)" }
- if (defined $msg) { die "Config file \"$config_file\" $msg," }
- $! = 0;
- if (defined(do $config_file)) {}
- elsif ($@ ne '') { die "Error in config file \"$config_file\": $@" }
- elsif ($! != 0) { die "Error reading config file \"$config_file\": $!" }
- }
- $daemon_chroot_dir = ''
- if !defined $daemon_chroot_dir || $daemon_chroot_dir eq '/';
- # provide some sensible defaults for essential settings (post-defaults)
- $TEMPBASE = $MYHOME if !defined $TEMPBASE;
- $helpers_home = $MYHOME if !defined $helpers_home;
- $db_home = "$MYHOME/db" if !defined $db_home;
- $lock_file = "$MYHOME/amavisd.lock" if !defined $lock_file;
- $pid_file = "$MYHOME/amavisd.pid" if !defined $pid_file;
- $X_HEADER_TAG = 'X-Virus-Scanned' if !defined $X_HEADER_TAG;
- $X_HEADER_LINE= "$myproduct_name at $mydomain" if !defined $X_HEADER_LINE;
- $gunzip = "$gzip -d" if !defined $gunzip && $gzip ne '';
- $bunzip2 = "$bzip2 -d" if !defined $bunzip2 && $bzip2 ne '';
- $unlzop = "$lzop -d" if !defined $unlzop && $lzop ne '';
- my($pname) = "\"Content-filter at $myhostname\"";
- $hdrfrom_notify_sender = "$pname <postmaster\@$myhostname>"
- if !defined $hdrfrom_notify_sender;
- $hdrfrom_notify_recip = $mailfrom_notify_recip ne ''
- ? "$pname <$mailfrom_notify_recip>"
- : $hdrfrom_notify_sender if !defined $hdrfrom_notify_recip;
- $hdrfrom_notify_admin = $mailfrom_notify_admin ne ''
- ? "$pname <$mailfrom_notify_admin>"
- : $hdrfrom_notify_sender if !defined $hdrfrom_notify_admin;
- $hdrfrom_notify_spamadmin = $mailfrom_notify_spamadmin ne ''
- ? "$pname <$mailfrom_notify_spamadmin>"
- : $hdrfrom_notify_sender if !defined $hdrfrom_notify_spamadmin;
- # compatibility with deprecated $warn*sender and old *_destiny values
- # map old values <0, =0, >0 into D_REJECT/D_BOUNCE, D_DISCARD, D_PASS
- for ($final_virus_destiny, $final_banned_destiny, $final_spam_destiny) {
- if ($_ > 0) { $_ = D_PASS }
- elsif ($_ < 0 && $_ != D_BOUNCE && $_ != D_REJECT) { # compatibility
- # favour Reject with sendmail milter, Bounce with others
- $_ = c('forward_method') eq '' ? D_REJECT : D_BOUNCE;
- }
- }
- if ($final_virus_destiny == D_DISCARD && c('warnvirussender') )
- { $final_virus_destiny = D_BOUNCE }
- if ($final_spam_destiny == D_DISCARD && c('warnspamsender') )
- { $final_spam_destiny = D_BOUNCE }
- if ($final_banned_destiny == D_DISCARD && c('warnbannedsender') )
- { $final_banned_destiny = D_BOUNCE }
- if ($final_bad_header_destiny == D_DISCARD && c('warnbadhsender') )
- { $final_bad_header_destiny = D_BOUNCE }
- if (!%banned_rules) {
- # an associative array mapping a rule name
- # to a single 'banned names/types' lookup table
- %banned_rules = ('DEFAULT'=>$banned_filename_re); # backwards compatibile
- }
- }
- 1;
- #
- package Amavis::Lock;
- use strict;
- use re 'taint';
- BEGIN {
- use Exporter ();
- use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.043';
- @ISA = qw(Exporter);
- @EXPORT = qw(&lock &unlock);
- }
- use Fcntl qw(LOCK_SH LOCK_EX LOCK_UN);
- use subs @EXPORT;
- sub lock($) {
- my($file_handle) = @_;
- flock($file_handle, LOCK_EX) or die "Can't lock $file_handle: $!";
- # NOTE: a lock is on a file, not on a file handle
- }
- sub unlock($) {
- my($file_handle) = @_;
- flock($file_handle, LOCK_UN) or die "Can't unlock $file_handle: $!";
- }
- 1;
- #
- package Amavis::Log;
- use strict;
- use re 'taint';
- BEGIN {
- use Exporter ();
- use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.043';
- @ISA = qw(Exporter);
- @EXPORT_OK = qw(&init &write_log &open_log &close_log &log_fd);
- }
- use subs @EXPORT_OK;
- use POSIX qw(locale_h strftime);
- use Unix::Syslog qw(:macros :subs);
- use IO::File ();
- use File::Basename;
- BEGIN {
- import Amavis::Conf qw(:platform $myversion $myhostname $daemon_user);
- import Amavis::Lock;
- }
- use vars qw($loghandle); # log file handle
- use vars qw($myname);
- use vars qw($syslog_facility $syslog_priority %syslog_priority);
- use vars qw($log_to_stderr $do_syslog $logfile);
- sub init($$$$) {
- my($syslog_level);
- ($log_to_stderr, $do_syslog, $syslog_level, $logfile) = @_;
- $myname = $0;
- if ($syslog_level =~ /^\s*([a-z0-9]+)\.([a-z0-9]+)\s*\z/i) {
- $syslog_facility = eval("LOG_\U$1");
- $syslog_priority = eval("LOG_\U$2");
- }
- $syslog_facility = LOG_DAEMON if $syslog_facility !~ /^\d+\z/;
- $syslog_priority = LOG_WARNING if $syslog_priority !~ /^\d+\z/;
- open_log();
- if (!$do_syslog && $logfile eq '')
- { print STDERR "Logging to STDERR (no \$LOGFILE and no \$DO_SYSLOG)\n" }
- my($msg) = "starting. $myname at $myhostname $myversion";
- $msg .= ", eol=\"$eol\"" if $eol ne "\n";
- $msg .= ", Unicode aware" if $unicode_aware;
- $msg .= ", LC_ALL=$ENV{LC_ALL}" if $ENV{LC_ALL} ne '';
- $msg .= ", LC_TYPE=$ENV{LC_TYPE}" if $ENV{LC_TYPE} ne '';
- $msg .= ", LC_CTYPE=$ENV{LC_CTYPE}" if $ENV{LC_CTYPE} ne '';
- $msg .= ", LANG=$ENV{LANG}" if $ENV{LANG} ne '';
- write_log(0, $msg, undef);
- }
- sub open_log() {
- # don't bother to skip opening the log even if $log_to_stderr (debug) is true
- if ($do_syslog) {
- openlog('amavis', LOG_PID | LOG_NDELAY, $syslog_facility);
- } elsif ($logfile ne '') {
- $loghandle = IO::File->new($logfile,'>>')
- or die "Failed to open log file $logfile: $!";
- $loghandle->autoflush(1);
- if ($> == 0) {
- my($uid) = $daemon_user=~/^(\d+)$/ ? $1 : (getpwnam($daemon_user))[2];
- if ($uid) {
- chown($uid,-1,$logfile)
- or die "Can't chown logfile $logfile to $uid: $!";
- }
- }
- }
- }
- sub close_log() {
- if ($do_syslog) {
- closelog();
- } elsif (defined($loghandle) && $logfile ne '') {
- $loghandle->close or die "Error closing log file $logfile: $!";
- $loghandle = undef;
- }
- }
- # Log either to syslog or to a file
- sub write_log($$$) {
- my($level,$errmsg,$am_id) = @_;
- $am_id = !defined $am_id ? '' : "($am_id) ";
- $errmsg = Amavis::Util::sanitize_str($errmsg);
- # my($old_locale) = POSIX::setlocale(LC_TIME,"C"); # English dates required!
- # if (length($errmsg) > 2000) { # crop at some arbitrary limit (< LINE_MAX)
- # $errmsg = substr($errmsg,0,2000) . "...";
- # }
- if ($do_syslog && !$log_to_stderr) {
- my($prio) = $syslog_priority; # never go below this priority level
- # syslog priorities: DEBUG, INFO, NOTICE, WARNING, ERR, CRIT, ALERT, EMERG
- if ($level <= -3) { $prio = LOG_CRIT if $prio > LOG_CRIT }
- elsif ($level <= -2) { $prio = LOG_ERR if $prio > LOG_ERR }
- elsif ($level <= -1) { $prio = LOG_WARNING if $prio > LOG_WARNING }
- elsif ($level <= 0) { $prio = LOG_NOTICE if $prio > LOG_NOTICE }
- elsif ($level <= 2) { $prio = LOG_INFO if $prio > LOG_INFO }
- else { $prio = LOG_DEBUG if $prio > LOG_DEBUG }
- my($pre) = '';
- my($logline_size) = 980; # less than (1023 - prefix)
- while (length($am_id)+length($pre)+length($errmsg) > $logline_size) {
- my($avail) = $logline_size - length($am_id . $pre . "...");
- syslog($prio, "%s", $am_id . $pre . substr($errmsg,0,$avail) . "...");
- $pre = "...";
- $errmsg = substr($errmsg, $avail);
- }
- syslog($prio, "%s", $am_id . $pre . $errmsg);
- } else {
- my($prefix) = sprintf("%s %s %s[%s]: ", # prepare syslog-alike prefix
- strftime("%b %e %H:%M:%S",localtime), $myhostname, $myname, $$);
- if (defined $loghandle && !$log_to_stderr) {
- lock($loghandle);
- seek($loghandle,0,2) or die "Can't position log file to its tail: $!";
- $loghandle->print($prefix, $am_id, $errmsg, $eol)
- or die "Error writing to log file: $!";
- unlock($loghandle);
- } else {
- print STDERR $prefix, $am_id, $errmsg, $eol
- or die "Error writing to STDERR: $!";
- }
- }
- # POSIX::setlocale(LC_TIME, $old_locale);
- }
- sub log_fd() {
- $log_to_stderr ? fileno(STDERR)
- : $do_syslog ? undef # how to obtain fd on syslog?
- : defined $loghandle ? $loghandle->fileno : fileno(STDERR);
- }
- 1;
- #
- package Amavis::Timing;
- use strict;
- use re 'taint';
- BEGIN {
- use Exporter ();
- use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.043';
- @ISA = qw(Exporter);
- @EXPORT_OK = qw(&init §ion_time &report &get_time_so_far);
- }
- use subs @EXPORT_OK;
- use Time::HiRes 1.49 ();
- use vars qw(@timing);
- # clear array @timing and enter start time
- sub init() {
- @timing = (); section_time('init');
- }
- # enter current time reading into array @timing
- sub section_time($) {
- push(@timing,shift,Time::HiRes::time);
- }
- # returns a string - a report of elapsed time by section
- sub report() {
- section_time('rundown');
- my($notneeded, $t0) = (shift(@timing), shift(@timing));
- my($total) = $t0 <= 0 ? 0 : $timing[$#timing] - $t0;
- if ($total < 0.0000001) { $total = 0.0000001 }
- my(@sections); my($t00) = $t0;
- while (@timing) {
- my($section, $t) = (shift(@timing), shift(@timing));
- my($dt) = $t <= $t0 ? 0 : $t-$t0; # handle possible clock jumps
- my($dt_c) = $t <= $t00 ? 0 : $t-$t00; # handle possible clock jumps
- my($dtp) = $dt >= $total ? 100 : $dt*100.0/$total; # this event
- my($dtp_c) = $dt_c >= $total ? 100 : $dt_c*100.0/$total; # cumulative
- push(@sections, sprintf("%s: %.0f (%.0f%%)%.0f",
- $section, $dt*1000, $dtp, $dtp_c));
- $t0 = $t;
- }
- sprintf("TIMING [total %.0f ms] - %s", $total * 1000, join(", ",@sections));
- }
- # returns value in seconds of elapsed time for processing of this mail so far
- sub get_time_so_far() {
- my($notneeded, $t0) = @timing;
- my($total) = $t0 <= 0 ? 0 : Time::HiRes::time - $t0;
- $total < 0 ? 0 : $total;
- }
- use vars qw($t_was_busy $t_busy_cum $t_idle_cum $t0);
- sub idle_proc(@) {
- my($t1) = Time::HiRes::time;
- if (defined $t0) {
- ($t_was_busy ? $t_busy_cum : $t_idle_cum) += $t1 - $t0;
- Amavis::Util::ll(5) && Amavis::Util::do_log(5,
- sprintf("idle_proc, @_: was %s, %.1f ms, total idle %.3f s, busy %.3f s",
- $t_was_busy ? "busy" : "idle", 1000 * ($t1 - $t0),
- $t_idle_cum, $t_busy_cum));
- }
- $t0 = $t1;
- }
- sub go_idle(@) {
- if ($t_was_busy) { idle_proc(@_); $t_was_busy = 0 }
- }
- sub go_busy(@) {
- if (!$t_was_busy) { idle_proc(@_); $t_was_busy = 1 }
- }
- sub report_load() {
- return if $t_busy_cum + $t_idle_cum <= 0;
- Amavis::Util::do_log(3, sprintf(
- "load: %.0f %%, total idle %.3f s, busy %.3f s",
- 100*$t_busy_cum / ($t_busy_cum + $t_idle_cum), $t_idle_cum, $t_busy_cum));
- }
- 1;
- #
- package Amavis::Util;
- use strict;
- use re 'taint';
- BEGIN {
- use Exporter ();
- use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.043';
- @ISA = qw(Exporter);
- @EXPORT_OK = qw(&untaint &min &max &safe_encode &safe_decode &q_encode
- &snmp_count &snmp_counters_init &snmp_counters_get
- &am_id &new_am_id &ll &do_log &debug_oneshot
- &add_entropy &fetch_entropy &generate_mail_id
- &retcode &exit_status_str &prolong_timer
- &sanitize_str &fmt_struct &strip_tempdir &rmdir_recursively
- &read_text &read_l10n_templates &read_hash &read_array
- &dump_hash &dump_array &run_command &run_command_consumer);
- }
- use subs @EXPORT_OK;
- use POSIX qw(WIFEXITED WIFSIGNALED WIFSTOPPED
- WEXITSTATUS WTERMSIG WSTOPSIG);
- use Errno qw(ENOENT EACCES);
- use Digest::MD5 2.22; # need 'clone' method
- # use Encode; # Perl 5.8 UTF-8 support
- BEGIN {
- import Amavis::Conf qw(:platform $DEBUG c cr ca);
- import Amavis::Log qw(write_log open_log close_log log_fd);
- import Amavis::Timing qw(section_time);
- }
- # Return untainted copy of a string (argument can be a string or a string ref)
- sub untaint($) {
- no re 'taint';
- my($str);
- if (defined($_[0])) {
- local($1); # avoid Perl taint bug: tainted global $1 propagates taintedness
- $str = $1 if (ref($_[0]) ? ${$_[0]} : $_[0]) =~ /^(.*)\z/s;
- }
- $str;
- }
- # Returns the smallest defined number from the list, or undef
- sub min(@) {
- my($r) = @_ == 1 && ref($_[0]) ? $_[0] : \@_; # accept list, or a list ref
- my($m); for (@$r) { $m = $_ if defined $_ && (!defined $m || $_ < $m) }
- $m;
- }
- # Returns the largest defined number from the list, or undef
- sub max(@) {
- my($r) = @_ == 1 && ref($_[0]) ? $_[0] : \@_; # accept list, or a list ref
- my($m); for (@$r) { $m = $_ if defined $_ && (!defined $m || $_ > $m) }
- $m;
- }
- # A wrapper for Encode::encode, avoiding a bug in Perl 5.8.0 which causes
- # Encode::encode to loop and fill memory when given a tainted string
- #
- # hmh@d.o : in Debian's 5.8.4-2, trying to restore the taintedness
- # actually causes perl to somehow lose track of the encoding and it
- # completely breaks this sub. OTOH, perl does loop eating up memory
- # on tainted strings, so we will have to lose taint state for now.
- sub safe_encode($$;$) {
- if (!$unicode_aware) { $_[1] } # just return the second argument
- else {
- my($encoding,$str,$check) = @_;
- $check = 0 if !defined($check);
- $str = untaint(\$str);
- return Encode::encode($encoding, $str, $check); # reattach taintedness
- # # taintedness of the string, with UTF-8 flag unconditionally off
- # my($taint) = Encode::encode('ascii',substr($str,0,0));
- # $taint . Encode::encode($encoding,untaint($str),$check); # preserve taint
- }
- }
- sub safe_decode($$;$) {
- if (!$unicode_aware) { $_[1] } # just return the second argument
- else {
- my($encoding,$str,$check) = @_;
- $check = 0 if !defined($check);
- my($taint) = substr($str,0,0); # taintedness of the string
- $taint . Encode::decode($encoding,untaint($str),$check); # preserve taint
- }
- }
- # Do the Q-encoding manually, the MIME::Words::encode_mimeword does not
- # encode spaces and does not limit to 75 ch, which violates the RFC 2047
- sub q_encode($$$) {
- my($octets,$encoding,$charset) = @_;
- my($prefix) = '=?' . $charset . '?' . $encoding . '?';
- my($suffix) = '?='; local($1,$2,$3);
- # FWS | utext (= NO-WS-CTL|rest of US-ASCII)
- $octets =~ /^ ( [\001-\011\013\014\016-\177]* [ \t] )? (.*?)
- ( [ \t] [\001-\011\013\014\016-\177]* )? \z/sx;
- my($head,$rest,$tail) = ($1,$2,$3);
- # Q-encode $rest according to RFC 2047
- # more restricted than =?_ so that it may be used in 'phrase'
- $rest =~ s{([^ 0-9a-zA-Z!*/+-])}{sprintf('=%02X',ord($1))}egs;
- $rest =~ tr/ /_/; # turn spaces into _ (rfc2047 allows it)
- my($s) = $head; my($len) = 75 - (length($prefix)+length($suffix)) - 2;
- while ($rest ne '') {
- $s .= ' ' if $s !~ /[ \t]\z/; # encoded words must be separated by FWS
- $rest =~ /^ ( .{0,$len} [^=] (?: [^=] | \z ) ) (.*) \z/sx;
- $s .= $prefix.$1.$suffix; $rest = $2;
- }
- $s.$tail;
- }
- # Set or get Amavis internal message id.
- # This message id performs a similar function as queue-id in MTA responses.
- # It may only be used in generating text part of SMTP responses,
- # or in generating log entries. It is only unique within a limited timespan.
- use vars qw($amavis_task_id); # internal message id (accessible via &am_id)
- sub am_id(;$) {
- if (@_) { # set, if argument present
- $amavis_task_id = shift;
- $0 = "amavisd ($amavis_task_id)";
- }
- $amavis_task_id; # return current value
- }
- sub new_am_id($;$$) {
- my($str, $cnt, $seq) = @_;
- my($id);
- $id = defined $str ? $str : sprintf("%05d", $$);
- $id .= sprintf("-%02d", $cnt) if defined $cnt;
- $id .= "-$seq" if defined $seq && $seq > 1;
- am_id($id);
- }
- use vars qw($entropy); # MD5 ctx (128 bits, 32 hex digits or 22 base64 chars)
- sub add_entropy(@) {
- $entropy = Digest::MD5->new if !defined $entropy;
- my($s) = join(",", map {!defined($_) ? 'U' : ref eq 'ARRAY' ? @$_ : $_} @_);
- # do_log(5,"add_entropy: ".$s);
- $entropy->add($s);
- }
- sub fetch_entropy() {
- $entropy->clone->b64digest;
- }
- # generate a reasonably unique (long-term) id based on collected entropy.
- # The result is a pair of (mostly public) mail_id, and a secret id,
- # where mail_id == b64(md5(b64(secret))). The secret id could be used to
- # authorize releasing quarantined mail. Both the mail_id and secret are
- # 12-char strings of characters [A-Za-z0-9+-], with an additional restriction
- # for mail_id which must begin and end with an alphanumeric character.
- sub generate_mail_id() {
- my($secret_id,$id,$rest);
- for (my $j=0; $j<100; $j++) { # provide some sanity loop limit just in case
- # take 72 bits from entropy accum. to produce a secret id, leave 56 bits
- local($1,$2); $entropy->clone->b64digest =~ /^(.{12})(.*)\z/s;
- ($secret_id,$rest) = ($1,$2); $secret_id =~ tr{/}{-}; # [A-Za-z0-9+-]
- # mail_id computed as md5(secret_id), rely on unidirectionality of md5
- $id = Digest::MD5->new->add($secret_id)->b64digest; # md5(b64(secret_id))
- last if $id =~ /^[A-Za-z0-9].{10}[A-Za-z0-9]/s; # starts&ends with alfnum
- add_entropy($j); # retry on less than 7% of cases
- do_log(5,"generate_mail_id retry: $id");
- }
- # start with a fresh entropy accumulator, wiping out traces of secret id
- $entropy = undef;
- add_entropy($rest); # carry over unused portion of old entropy accumulator
- add_entropy($id); # mix-in the full mail_id before chopping it to 12 chars
- $id = substr($id,0,12); $id =~ tr{/}{-};
- ($id,$secret_id);
- }
- use vars qw(@counter_names);
- # elements may be counter names (increment is 1), or pairs: [name,increment]
- sub snmp_counters_init() { @counter_names = () }
- sub snmp_count(@) { push(@counter_names, @_) }
- sub snmp_counters_get() { \@counter_names }
- use vars qw($debug_oneshot);
- sub debug_oneshot(;$$) {
- if (@_) {
- my($new_debug_oneshot) = shift;
- if (($new_debug_oneshot ? 1 : 0) != ($debug_oneshot ? 1 : 0)) {
- do_log(0, "DEBUG_ONESHOT: TURNED ".($new_debug_oneshot ? "ON" : "OFF"));
- do_log(0, shift) if @_; # caller-provided extra log entry, usually
- # the one that caused debug_oneshot call
- }
- $debug_oneshot = $new_debug_oneshot;
- }
- $debug_oneshot;
- }
- # is a message log level below the current log level?
- sub ll($) {
- my($level) = @_;
- $level = 0 if $level > 0 && ($DEBUG || $debug_oneshot);
- my($current_log_level) = c('log_level');
- $current_log_level = 0 if !defined($current_log_level);
- $level <= $current_log_level;
- }
- # write log entry
- sub do_log($$) {
- my($level, $errmsg) = @_;
- if (ll($level)) {
- $level = 0 if $level > 0 && ($DEBUG || $debug_oneshot);
- write_log($level, $errmsg, am_id());
- }
- }
- sub retcode($) { # (this subroutine is being phased out)
- my $code = shift;
- return WEXITSTATUS($code) if WIFEXITED($code);
- return 128 + WTERMSIG($code) if WIFSIGNALED($code);
- return 255;
- }
- # map process termination status number to a string, and append optional
- # user error mesage, returning the resulting string
- sub exit_status_str($;$) {
- my($stat,$err) = @_; my($str);
- if (WIFEXITED($stat)) {
- $str = sprintf("exit %d", WEXITSTATUS($stat));
- } elsif (WIFSTOPPED($stat)) {
- $str = sprintf("stopped, signal %d", WSTOPSIG($stat));
- } else {
- $str = sprintf("DIED on signal %d (%04x)", WTERMSIG($stat),$stat);
- }
- $str .= ', '.$err if defined $err && $err ne '';
- $str;
- }
- sub prolong_timer($;$) {
- my($which_section, $child_remaining_time) = @_;
- if (!defined($child_remaining_time)) {
- $child_remaining_time = alarm(0); # check how much time is left
- }
- do_log(4, "prolong_timer after $which_section: "
- . "remaining time = $child_remaining_time s");
- $child_remaining_time = 60 if $child_remaining_time < 60;
- alarm($child_remaining_time); # restart/prolong the timer
- }
- # Mostly for debugging and reporting purposes:
- # Convert nonprintable characters in the argument
- # to \[rnftbe], or \octal code, and '\' to '\\',
- # and Unicode characters to \x{xxxx}, returning the sanitized string.
- sub sanitize_str {
- my($str, $keep_eol) = @_;
- my(%map) = ("\r" => '\\r', "\n" => '\\n', "\f" => '\\f', "\t" => '\\t',
- "\b" => '\\b', "\e" => '\\e', "\\" => '\\\\');
- if ($keep_eol) {
- $str =~ s/([^\012\040-\133\135-\176])/ # and \240-\376 ?
- exists($map{$1}) ? $map{$1} :
- sprintf(ord($1)>255 ? '\\x{%04x}' : '\\%03o', ord($1))/eg;
- } else {
- $str =~ s/([^\040-\133\135-\176])/ # and \240-\376 ?
- exists($map{$1}) ? $map{$1} :
- sprintf(ord($1)>255 ? '\\x{%04x}' : '\\%03o', ord($1))/eg;
- }
- $str;
- }
- # pretty-print a structure for logging purposes: returns a string
- sub fmt_struct($) {
- my($arg) = @_;
- !defined($arg) ? 'undef' : !ref($arg) ? '"'.$arg.'"' :
- ref($arg) eq 'ARRAY' ? '['.join(',',map {fmt_struct($_)} @$arg).']' : $arg;
- };
- # Checks tempdir after being cleaned.
- # It may only contain subdirectory 'parts' and file email.txt, nothing else.
- #
- sub check_tempdir($) {
- my($dir) = shift;
- local(*DIR); opendir(DIR,$dir) or die "Can't open directory $dir: $!";
- eval {
- undef $!, my($f);
- while (defined($f = readdir(DIR))) {
- if (!-d ("$dir/$f")) {
- die "Unexpected file $dir/$f" if $f ne 'email.txt';
- } elsif ($f eq '.' || $f eq '..' || $f eq 'parts') {
- } else {
- die "Unexpected subdirectory $dir/$f";
- }
- }
- # $!==0 or die "Error reading directory $dir: $!";
- };
- closedir(DIR) or die "Error closing directory $dir: $!";
- if ($@ ne '') { chomp($@); die "check_tempdir: $@\n" }
- 1;
- }
- # Remove all files and subdirectories from the temporary directory, leaving
- # only the directory itself, file email.txt, and empty subdirectory ./parts .
- # Leaving directories for reuse represents an important saving in time,
- # as directory creation + deletion is quite an expensive operation,
- # requiring atomic file system operation, including flushing buffers to disk.
- #
- sub strip_tempdir($) {
- my($dir) = shift;
- do_log(4, "strip_tempdir: $dir");
- my($errn) = lstat("$dir/parts") ? 0 : 0+$!;
- if ($errn == ENOENT) {} # fine, no such directory
- elsif ($errn != 0) { die "strip_tempdir: error accessing $dir/parts: $!" }
- elsif ( -l _) { die "strip_tempdir: $dir/parts is a symbolic link" }
- elsif (!-d _) { die "strip_tempdir: $dir/parts is not a directory" }
- else { rmdir_recursively("$dir/parts", 1) }
- # All done. Check for any remains in the top directory just in case
- check_tempdir($dir);
- 1;
- }
- #
- # Removes a directory, along with its contents
- sub rmdir_recursively($;$); # prototype
- sub rmdir_recursively($;$) {
- my($dir, $exclude_itself) = @_; my($cnt) = 0;
- do_log(4,"rmdir_recursively: $dir, excl=$exclude_itself");
- local(*DIR); my($errn) = opendir(DIR,$dir) ? 0 : 0+$!;
- if ($errn == ENOENT) { die "Directory $dir does not exist," }
- elsif ($errn == EACCES) { # relax protection on directory, then try again
- do_log(3,"rmdir_recursively: enabling read access to directory $dir");
- chmod(0750,$dir) or die "Can't change protection-1 on dir $dir: $!";
- $errn = opendir(DIR,$dir) ? 0 : 0+$!; # try again
- }
- if ($errn) { die "Can't open directory $dir: $!" }
- my(@dirfiles) = readdir(DIR); # must avoid modifying dir. while traversing it
- closedir(DIR) or die "Error closing directory $dir: $!";
- for my $f (@dirfiles) {
- my($fname) = "$dir/$f";
- $errn = lstat($fname) ? 0 : 0+$!;
- if ($errn == ENOENT) { die "File \"$fname\" does not exist" }
- elsif ($errn == EACCES) { # relax protection on the directory and retry
- do_log(3,"rmdir_recursively: enabling access to files in dir $dir");
- chmod(0750,$dir) or die "Can't change protection-2 on dir $dir: $!";
- $errn = lstat($fname) ? 0 : 0+$!; # try again
- }
- if ($errn) { die "File \"$fname\" inaccessible: $!" }
- next if ($f eq '.' || $f eq '..') && -d _;
- if (-d _) { rmdir_recursively(untaint($fname), 0) }
- else {
- $cnt++;
- if (unlink(untaint($fname))) { # ok
- } else { # relax protection on the directory, then try again
- do_log(3,"rmdir_recursively: enabling write access to dir $dir");
- my($what) = -l _ ? 'symlink' :-d _ ? 'directory' :'non-regular file';
- chmod(0750,$dir) or die "Can't change protection-3 on dir $dir: $!";
- unlink(untaint($fname)) or die "Can't remove $what $fname: $!";
- }
- }
- }
- section_time("unlink-$cnt-files");
- if (!$exclude_itself) {
- rmdir($dir) or die "rmdir_recursively: Can't remove directory $dir: $!";
- section_time('rmdir');
- }
- 1;
- }
- # read a multiline string from a file - may be called from amavisd.conf
- sub read_text($;$) {
- my($filename, $encoding) = @_;
- my($inp) = IO::File->new;
- $inp->open($filename,'<') or die "Can't open file $filename for reading: $!";
- if ($unicode_aware && $encoding ne '') {
- binmode($inp, ":encoding($encoding)")
- or die "Can't set :encoding($encoding) on file $filename: $!";
- }
- my($str) = ''; # must not be undef, work around a Perl UTF8 bug
- my($nbytes,$buff);
- while (($nbytes=$inp->read($buff,16384)) > 0) { $str .= $buff }
- defined $nbytes or die "Error reading from $filename: $!";
- $inp->close or die "Error closing $filename: $!";
- $str;
- }
- # attempt to read all user-visible replies from a l10n dir
- # This function auto-fills $notify_sender_templ, $notify_virus_sender_templ,
- # $notify_virus_admin_templ, $notify_virus_recips_templ,
- # $notify_spam_sender_templ and $notify_spam_admin_templ from files named
- # template-dsn.txt, template-virus-sender.txt, template-virus-admin.txt,
- # template-virus-recipient.txt, template-spam-sender.txt,
- # template-spam-admin.txt. If this is available, it uses the charset
- # file to do automatic charset conversion. Used by the Debian distribution.
- sub read_l10n_templates($;$) {
- my($dir) = @_;
- if (@_ > 1) # compatibility with Debian
- { my($l10nlang, $l10nbase) = @_; $dir = "$l10nbase/$l10nlang" }
- my($file_chset) = Amavis::Util::read_text("$dir/charset");
- if ($file_chset =~ m{^(?:#[^\n]*\n)*([^./\n\s]+)(\s*[#\n].*)?$}s) {
- $file_chset = untaint($1);
- } else {
- die "Invalid charset $file_chset\n";
- }
- $Amavis::Conf::notify_sender_templ =
- Amavis::Util::read_text("$dir/template-dsn.txt", $file_chset);
- $Amavis::Conf::notify_virus_sender_templ =
- Amavis::Util::read_text("$dir/template-virus-sender.txt", $file_chset);
- $Amavis::Conf::notify_virus_admin_templ =
- Amavis::Util::read_text("$dir/template-virus-admin.txt", $file_chset);
- $Amavis::Conf::notify_virus_recips_templ =
- Amavis::Util::read_text("$dir/template-virus-recipient.txt", $file_chset);
- $Amavis::Conf::notify_spam_sender_templ =
- Amavis::Util::read_text("$dir/template-spam-sender.txt", $file_chset);
- $Amavis::Conf::notify_spam_admin_templ =
- Amavis::Util::read_text("$dir/template-spam-admin.txt", $file_chset);
- }
- #use CDB_File;
- #sub tie_hash($$) {
- # my($hashref, $filename) = @_;
- # CDB_File::create(%$hashref, $filename, "$filename.tmp$$")
- # or die "Can't create cdb $filename: $!";
- # my($cdb) = tie(%$hashref,'CDB_File',$filename)
- # or die "Tie to $filename failed: $!";
- # $hashref;
- #}
- # read a lookup associative array (Perl hash) from a file - may be called
- # from amavisd.conf
- #
- # Format: one key per line, anything from '#' to the end of line
- # is considered a comment, but '#' within correctly quoted rfc2821
- # addresses is not treated as a comment (e.g. a hash sign within
- # "strange # \"foo\" address"@example.com is part of the string).
- # Lines may contain a pair: key value, separated by whitespace, or key only,
- # in which case a value 1 is implied. Trailing whitespace is discarded,
- # empty lines (containing only whitespace and comment) are ignored.
- # Addresses (lefthand-side) are converted from rfc2821-quoted form
- # into internal (raw) form and inserted as keys into a given hash.
- # NOTE: the format is partly compatible with Postfix maps (not aliases):
- # no continuation lines are honoured, Postfix maps do not allow
- # rfc2821-quoted addresses containing whitespace, Postfix only allows
- # comments starting at the beginning of a line.
- #
- # The $hashref argument is returned for convenience, so that one can do
- # for example:
- # $per_recip_whitelist_sender_lookup_tables = {
- # '.my1.example.com' => read_hash({},'/var/amavis/my1-example-com.wl'),
- # '.my2.example.com' => read_hash({},'/var/amavis/my2-example-com.wl') }
- # or even simpler:
- # $per_recip_whitelist_sender_lookup_tables = {
- # '.my1.example.com' => read_hash('/var/amavis/my1-example-com.wl'),
- # '.my2.example.com' => read_hash('/var/amavis/my2-example-com.wl') }
- #
- sub read_hash(@) {
- unshift(@_,{}) if !ref $_[0]; # first argument is optional, defaults to {}
- my($hashref, $filename, $keep_case) = @_;
- my($lpcs) = c('localpart_is_case_sensitive');
- my($inp) = IO::File->new;
- $inp->open($filename,'<') or die "Can't open file $filename for reading: $!";
- my($ln);
- for (undef $!; defined($ln=$inp->getline); undef $!) {
- chomp($ln);
- # carefully handle comments, '#' within "" does not count as a comment
- my($lhs) = ''; my($rhs) = ''; my($at_rhs) = 0;
- for my $t ( $ln =~ /\G ( " (?: \\. | [^"\\] )* " |
- [^#" \t]+ | [ \t]+ | . )/gcsx) {
- last if $t eq '#';
- if (!$at_rhs && $t =~ /^[ \t]+\z/) { $at_rhs = 1 }
- else { ($at_rhs ? $rhs : $lhs) .= $t }
- }
- $rhs =~ s/[ \t]+\z//; # trim trailing whitespace
- next if $lhs eq '' && $rhs eq '';
- my($addr) = Amavis::rfc2821_2822_Tools::unquote_rfc2821_local($lhs);
- my($localpart,$domain) = Amavis::rfc2821_2822_Tools::split_address($addr);
- $localpart = lc($localpart) if !$lpcs;
- $addr = $localpart . lc($domain);
- $hashref->{$addr} = $rhs eq '' ? 1 : $rhs;
- # do_log(5, "read_hash: address: <$addr>: ".$hashref->{$addr});
- }
- defined $ln || $!==0 or die "Error reading from $filename: $!";
- $inp->close or die "Error closing $filename: $!";
- $hashref;
- }
- sub read_array(@) {
- unshift(@_,[]) if !ref $_[0]; # first argument is optional, defaults to []
- my($arrref, $filename, $keep_case) = @_;
- my($inp) = IO::File->new;
- $inp->open($filename,'<') or die "Can't open file $filename for reading: $!";
- my($ln);
- for (undef $!; defined($ln=$inp->getline); undef $!) {
- chomp($ln); my($lhs) = '';
- # carefully handle comments, '#' within "" does not count as a comment
- for my $t ( $ln =~ /\G ( " (?: \\. | [^"\\] )* " |
- [^#" \t]+ | [ \t]+ | . )/gcsx) {
- last if $t eq '#';
- $lhs .= $t;
- }
- $lhs =~ s/[ \t]+\z//; # trim trailing whitespace
- push(@$arrref, Amavis::rfc2821_2822_Tools::unquote_rfc2821_local($lhs))
- if $lhs ne '';
- }
- defined $ln || $!==0 or die "Error reading from $filename: $!";
- $inp->close or die "Error closing $filename: $!";
- $arrref;
- }
- sub dump_hash($) {
- my($hr) = @_;
- do_log(0, sprintf("dump_hash: %s => %s", $_,$hr->{$_})) for (sort keys %$hr);
- }
- sub dump_array($) {
- my($ar) = @_;
- do_log(0, sprintf("dump_array: %s", $_)) for @$ar;
- }
- # Run specified command as a subprocess. Return a file handle open for
- sub run_command($$@) {
- my($stdin_from, $stderr_to, $cmd, @args) = @_;
- my($cmd_text) = join(' ', $cmd, @args);
- $stdin_from = '/dev/null' if $stdin_from eq '';
- $stderr_to = '/dev/null' if defined($stderr_to) && $stderr_to eq '';
- my($msg) = join(' ', $cmd, @args, "<$stdin_from",
- $stderr_to eq '' ? () : "2>$stderr_to");
- # $^F == 2 or do_log(-1,"run_command: SYSTEM_FD_MAX not 2: %d", $^F);
- my($pid); my($proc_fh) = IO::File->new;
- eval {
- $pid = $proc_fh->open('-|'); 1; # fork, catching errors
- } or do {
- my($eval_stat) = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
- die "run_command (open pipe): $eval_stat";
- };
- defined($pid) or die "run_command: can't fork: $!";
- if (!$pid) { # child
- alarm(0); my($interrupt) = '';
- my($h1) = sub { $interrupt = $_[0] };
- my($h2) = sub { die "Received signal ".$_[0] };
- @SIG{qw(INT HUP TERM TSTP QUIT USR1 USR2)} = ($h1) x 7;
- eval { # die must be caught, otherwise we end up with two running daemons
- local(@SIG{qw(INT HUP TERM TSTP QUIT USR1 USR2)}) = ($h2) x 7;
- if ($interrupt ne '') { my($i) = $interrupt; $interrupt = ''; die $i }
- # use Devel::Symdump ();
- # my($dumpobj) = Devel::Symdump->rnew;
- # for my $k ($dumpobj->ios) {
- # no strict 'refs'; my($fn) = fileno($k);
- # if (!defined($fn)) { do_log(2, "not open %s", $k) }
- # elsif ($fn == 1 || $fn == 2) { do_log(2, "KEEP %s, fileno=%s",$k,$fn) }
- # else { $! = 0;
- # close(*{$k}{IO}) and do_log(2, "DID CLOSE %s (fileno=%s)", $k,$fn);
- # }
- # }
- release_parent_resources();
- open_on_specific_fd(0,$stdin_from,&POSIX::O_RDONLY,0);
- open_on_specific_fd(2,$stderr_to,&POSIX::O_WRONLY,0) if $stderr_to ne '';
- # eval { close_log() }; # may have been closed by open_on_specific_fd
- # BEWARE of Perl older that 5.6.0: sockets and pipes were not FD_CLOEXEC
- exec {$cmd} ($cmd,@args);
- die "run_command: failed to exec $cmd_text: $!";
- };
- my($err) = $@ ne '' ? $@ : "errno=$!"; chomp $err;
- eval {
- local(@SIG{qw(INT HUP TERM TSTP QUIT USR1 USR2)}) = ($h2) x 7;
- if ($interrupt ne '') { my($i) = $interrupt; $interrupt = ''; die $i }
- open_log(); # oops, exec failed, we will need logging after all...
- # we're in trouble if stderr was attached to a terminal, but no longer is
- do_log(-1,sprintf("run_command: child process [%s]: %s", $$,$err));
- };
- { no warnings;
- POSIX::_exit(8); # avoid END and destructor processing
- kill('KILL',$$); exit 1; # still kicking? die!
- }
- }
- # parent
- ll(5) && do_log(5,sprintf("run_command: [%s] %s", $pid,$msg));
- binmode($proc_fh) or die "Can't set pipe to binmode: $!"; # dflt Perl 5.8.1
- ($proc_fh, $pid); # return pipe file handle to the subprocess and its PID
- }
- # POSIX::open a file or dup an existing fd (Perl open syntax), with a
- # requirement that it gets opened on a prescribed file descriptor $fd_target;
- # this subroutine is usually called from a forked process prior to exec
- sub open_on_specific_fd($$$$) {
- my($fd_target,$fname,$flags,$mode) = @_;
- my($fd_got); # fd directy given as argument, or obtained from POSIX::open
- my($logging_safe) = 0;
- if (ll(5)) {
- # crude attempt to prevent a forked process from writing log records
- # to its parent process on STDOUT or STDERR
- my($log_fd) = log_fd();
- $logging_safe = 1 if !defined($log_fd) || $log_fd > 2;
- }
- local($1);
- if ($fname =~ /^&=?(\d+)\z/) { $fd_got = $1 } # fd directly specified
- my($flags_displayed) = $flags == &POSIX::O_RDONLY ? '<'
- : $flags == &POSIX::O_WRONLY ? '>' : $flags;
- if (!defined($fd_got) || $fd_got != $fd_target) {
- # close whatever is on a target descriptor but don't shoot self in the foot
- # with Net::Server <= 0.90 fd0 was main::stdin, but no longer is in 0.91
- do_log(5, sprintf("open_on_specific_fd: target fd%s closing, to become %s %s",
- $fd_target,$flags_displayed,$fname)) if $logging_safe;
- # it pays off to close explicitly, with some luck open will get a target fd
- POSIX::close($fd_target); # ignore error, we may have just closed a log
- }
- if (!defined($fd_got)) { # file name was given, not a descriptor
- $fd_got = POSIX::open($fname,$flags,$mode);
- defined $fd_got or die "Can't open $fname: $!";
- $fd_got = 0 + $fd_got; # turn into numeric, avoid: "0 but true"
- }
- if ($fd_got != $fd_target) { # dup, ensuring we get a specified descriptor
- eval { # we may have been left without a log file descriptor, must not die
- do_log(5, sprintf("open_on_specific_fd: target fd%s dup2 from fd%s %s %s",
- $fd_target,$fd_got,$flags_displayed,$fname)) if $logging_safe;
- };
- # POSIX mandates we got the lowest fd available (but some kernels have
- # bugs), let's be explicit that we require a specified file descriptor
- defined POSIX::dup2($fd_got,$fd_target)
- or die "Can't dup2 from $fd_got to $fd_target: $!";
- if ($fd_got > 2) { # let's get rid of the original fd, unless 0,1,2
- my($err); defined POSIX::close($fd_got) or $err = $!;
- $err = defined $err ? ": $err" : '';
- eval { # we may have been left without a log file descriptor, don't die
- do_log(5, sprintf("open_on_specific_fd: source fd%s closed%s",
- $fd_got,$err)) if $logging_safe;
- };
- }
- }
- $fd_got;
- }
- sub release_parent_resources() {
- $Amavis::sql_dataset_conn_lookups->dbh_inactive(1)
- if $Amavis::sql_dataset_conn_lookups;
- $Amavis::sql_dataset_conn_storage->dbh_inactive(1)
- if $Amavis::sql_dataset_conn_storage;
- # undef $Amavis::sql_dataset_conn_lookups;
- # undef $Amavis::sql_dataset_conn_storage;
- # undef $Amavis::body_digest_cache; undef $Amavis::snmp_db;
- # undef $Amavis::db_env;
- }
- # WRITING to the subprocess. Use IO::Handle to ensure the subprocess
- # will be automatically reclaimed in case of failure.
- #
- sub run_command_consumer($$@) {
- my($stdout_to, $stderr_to, $cmd, @args) = @_;
- my($cmd_text) = join(' ', $cmd, @args);
- $stdout_to = '/dev/null' if $stdout_to eq '';
- my($msg) = join(' ', $cmd, @args, ">$stdout_to");
- $msg .= " 2>$stderr_to" if $stderr_to ne '';
- my($pid); my($proc_fh) = IO::File->new;
- eval { $pid = $proc_fh->open('|-') }; # fork, catching errors
- if ($@ ne '') { chomp($@); die "run_command_consumer (open pipe): $@" }
- defined($pid) or die "run_command_consumer: can't fork: $!";
- if (!$pid) { # child
- eval { # must not use die in forked process, or we end up with
- # two running daemons! Close unneeded files.
- # $sql_dataset_conn_lookups->dbh_inactive(1) if $sql_dataset_conn_lookups;
- # $sql_dataset_conn_storage->dbh_inactive(1) if $sql_dataset_conn_storage;
- # $sql_dataset_conn_lookups = $sql_dataset_conn_storage = undef;
- close_log();
- close(main::stderr) or die "Error closing main::stderr: $!";
- close(main::stdout) or die "Error closing main::stdout: $!";
- close(main::STDOUT) or die "Error closing main::STDOUT: $!";
- open(STDOUT, ">$stdout_to")
- or die "Can't reopen STDOUT on $stdout_to: $!";
- fileno(STDOUT) == 1
- or die ("run_command_consumer: STDOUT not fd1: ".fileno(STDOUT));
- if ($stderr_to ne '') {
- close(STDERR) or die "Error closing STDERR: $!";
- open(STDERR, ">$stderr_to")
- or die "Can't open STDERR to $stderr_to: $!";
- fileno(STDERR) == 2
- or die ("run_command_consumer: STDERR not fd2: ".fileno(STDERR));
- }
- # BEWARE of Perl older that 5.6.0: sockets and pipes were not FD_CLOEXEC
- { no warnings;
- exec {$cmd} ($cmd,@args) or die "Failed to exec $cmd_text: $!";
- }
- };
- my($err) = $@; chomp($err);
- eval {
- open_log(); # oops, exec failed, we will need logging after all...
- do_log(-2,"run_command_consumer: child process [$$]: $err\n");
- };
- { no warnings;
- POSIX::_exit(1); # avoid END and destructor processing
- kill('KILL',$$) # still kicking? die!
- or do_log(-3,"run_command_consumer: TROUBLE - Panic1, can't die: $!");
- do_log(-3,"run_command_consumer: TROUBLE - Panic2, can't die");
- exit 1; # better safe than sorry
- # NOTREACHED
- }
- }
- # parent
- do_log(5,"run_command_consumer: [$pid] $msg");
- binmode($proc_fh) or die "Can't set pipe to binmode: $!"; # dflt Perl 5.8.1
- ($proc_fh, $pid); # return pipe file handle to the subprocess and its PID
- }
- 1;
- #
- package Amavis::rfc2821_2822_Tools;
- use strict;
- use re 'taint';
- BEGIN {
- use Exporter ();
- use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.043';
- @ISA = qw(Exporter);
- @EXPORT = qw(
- &iso8601_timestamp &iso8601_utc_timestamp &rfc2822_timestamp
- &received_line &parse_received
- &fish_out_ip_from_received &split_address &split_localpart &make_query_keys
- "e_rfc2821_local &qquote_rfc2821_local &unquote_rfc2821_local
- &one_response_for_all
- &EX_OK &EX_NOUSER &EX_UNAVAILABLE &EX_TEMPFAIL &EX_NOPERM);
- }
- use subs @EXPORT;
- use POSIX qw(locale_h strftime);
- BEGIN {
- eval { require 'sysexits.ph' }; # try to use the installed version
- # define the most important constants if undefined
- do { sub EX_OK() {0} } unless defined(&EX_OK);
- do { sub EX_NOUSER() {67} } unless defined(&EX_NOUSER);
- do { sub EX_UNAVAILABLE() {69} } unless defined(&EX_UNAVAILABLE);
- do { sub EX_TEMPFAIL() {75} } unless defined(&EX_TEMPFAIL);
- do { sub EX_NOPERM() {77} } unless defined(&EX_NOPERM);
- }
- BEGIN {
- import Amavis::Conf qw(:platform $myhostname c cr ca);
- import Amavis::Util qw(ll do_log);
- }
- # Given a Unix time, return the local time zone offset at that time
- # as a string +HHMM or -HHMM, appropriate for the RFC2822 date format.
- # Works also for non-full-hour zone offsets, and on systems where strftime
- # can not return TZ offset as a number; (c) Mark Martinec, GPL
- #
- sub get_zone_offset($) {
- my($t) = @_;
- my($d) = 0; # local zone offset in seconds
- for (1..3) { # match the date (with a safety loop limit just in case)
- my($r) = sprintf("%04d%02d%02d", (localtime($t))[5, 4, 3]) cmp
- sprintf("%04d%02d%02d", (gmtime($t + $d))[5, 4, 3]);
- if ($r == 0) { last } else { $d += $r * 24 * 3600 }
- }
- my($sl,$su) = (0,0);
- for ((localtime($t))[2,1,0]) { $sl = $sl * 60 + $_ }
- for ((gmtime($t + $d))[2,1,0]) { $su = $su * 60 + $_ }
- $d += $sl - $su; # add HMS difference (in seconds)
- my($sign) = $d >= 0 ? '+' : '-';
- $d = -$d if $d < 0;
- $d = int(($d + 30) / 60.0); # give minutes, rounded
- sprintf("%s%02d%02d", $sign, int($d / 60), $d % 60);
- }
- # Given a Unix numeric time (seconds since 1970-01-01T00:00Z),
- # provide date-time timestamp (local time) as specified in ISO 8601 (EN 28601)
- #
- sub iso8601_timestamp($;$$) {
- my($t,$suppress_zone,$separator) = @_;
- # can't use %z because some systems do not support it (is treated as %Z)
- my($s) = strftime("%Y%m%dT%H%M%S", localtime($t));
- $s =~ s/T/$separator/ if defined $separator;
- $s .= get_zone_offset($t) unless $suppress_zone;
- $s;
- }
- # Given a Unix numeric time (seconds since 1970-01-01T00:00Z),
- # provide date-time timestamp (UTC) as specified in ISO 8601 (EN 28601)
- #
- sub iso8601_utc_timestamp($;$$) {
- my($t,$suppress_zone,$separator) = @_;
- my($s) = strftime("%Y%m%dT%H%M%S", gmtime($t));
- $s =~ s/T/$separator/ if defined $separator;
- $s .= 'Z' unless $suppress_zone;
- $s;
- }
- # Given a Unix time, provide date-time timestamp as specified in RFC 2822
- # (local time), to be used in header fields such as 'Date:' and 'Received:'
- #
- sub rfc2822_timestamp($) {
- my($t) = @_;
- my(@lt) = localtime($t);
- # can't use %z because some systems do not support it (is treated as %Z)
- # my($old_locale) = POSIX::setlocale(LC_TIME,"C"); # English dates required!
- my($zone_name) = strftime("%Z",@lt);
- my($s) = strftime("%a, %e %b %Y %H:%M:%S ", @lt);
- $s .= get_zone_offset($t);
- $s .= " (" . $zone_name . ")" if $zone_name !~ /^\s*\z/;
- # POSIX::setlocale(LC_TIME, $old_locale); # restore the locale
- $s;
- }
- sub received_line($$$$) {
- my($conn, $msginfo, $id, $folded) = @_;
- my($smtp_proto, $recips) = ($conn->smtp_proto, $msginfo->recips);
- my($client_ip) = $conn->client_ip;
- if ($client_ip =~ /:/ && $client_ip !~ /^IPv6:/i) {
- $client_ip = 'IPv6:' . $client_ip;
- }
- my($s) = sprintf("from %s%s\n by %s%s (amavisd-new, %s)",
- ($conn->smtp_helo eq '' ? 'unknown' : $conn->smtp_helo),
- ($client_ip eq '' ? '' : " ([$client_ip])"),
- c('localhost_name'),
- ($conn->socket_ip eq '' ? ''
- : sprintf(" (%s [%s])", $myhostname, $conn->socket_ip) ),
- ($conn->socket_port eq '' ? 'unix socket' : "port ".$conn->socket_port) );
- $s .= "\n with $smtp_proto" if $smtp_proto=~/^(ES|S|L)MTPS?A?\z/i; # rfc3848
- $s .= "\n id $id" if $id ne '';
- # do not disclose recipients if more than one
- $s .= "\n for " . qquote_rfc2821_local(@$recips) if @$recips == 1;
- $s .= ";\n " . rfc2822_timestamp($msginfo->rx_time);
- $s =~ s/\n//g if !$folded;
- $s;
- }
- sub parse_received($) {
- my($received) = @_;
- local($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11);
- $received =~ s/\n([ \t])/$1/g; # unfold
- $received =~ s/[\n\r]//g; # delete remaining newlines if any
- my(%fields);
- while ($received =~ m{\G\s*
- ( \b(from|by) \s+ ( (?: \[ (?: \\. | [^\]\\] )* \] | [^;\s\[] )+ )
- (?: \s* \( (?: ( [^\s\[]+ ) \s+ )?
- \[ ( (?: \\. | [^\]\\] )* ) \] \s*
- \) )?
- (?: .*? ) (?= \(|;|\z|\b(?:from|by|via|with|id|for)\b ) # junk
- | \b(via|with|id|for) \s+
- ( (?: " (?: \\. | [^"\\] )* "
- | \[ (?: \\. | [^\]\\] )* \]
- | \\. | [0-9a-z]+ | . # greedy words avoid deep recursion
- )+? (?= \(|;|\z|\b(?:from|by|via|with|id|for)\b ) )
- | (;) \s* ( .*? ) \s* \z # time
- | (.*?) (?= \(|;|\z|\b(?:from|by|via|with|id|for)\b ) # junk
- ) ( (?: \s+ | (?: \( (?: \\. | [^)\\] )* \) ) )* ) }xgcsi)
- {
- my($v1, $v2, $v3, $comment) = ('') x 4;
- my($item, $field) = ($1, lc($2 || $6 || $8));
- $field = '' if !defined($field); # mute a warning about uninit. value
- if ($field eq 'from' || $field eq 'by') {
- ($v1, $v2, $v3, $comment) = ($3, $4, $5, $11);
- } elsif ($field eq ';') { # time
- ($v1, $comment) = ($9, $11);
- } elsif (!defined($10) || $10 eq '') { # via|with|id|for
- ($v1, $comment) = ($7, $11);
- } else { # junk
- ($v1, $comment) = ($10, $11);
- }
- $comment =~ s/^\s+//;
- $comment =~ s/\s+\z//;
- $item =~ s/^\Q$field\E\s*//i;
- if (!exists $fields{$field}) {
- $fields{$field} = [$item, $v1, $v2, $v3, $comment];
- ll(5) && do_log(5, sprintf("parse_received: %s = %s/%s/%s/%s",
- map { !defined($_) ? '' : length($_) <= 50 ? $_
- : substr($_,0,50)."..." }
- ($field, @{$fields{$field}}) )) if $field ne '';
- }
- }
- \%fields;
- }
- sub fish_out_ip_from_received($) {
- my($received) = @_;
- my($ip);
- my($fields_ref) = parse_received($received);
- if (defined $fields_ref && exists $fields_ref->{'from'}) {
- my($item, $v1, $v2, $v3, $comment) = @{$fields_ref->{'from'}};
- for (map {defined $_ ? $_ : ''} ($v3, $v2, $v1, $comment, $item)) {
- if (/ \[ (\d{1,3} (?: \. \d{1,3}){3}) \] /x) {
- $ip = $1; last;
- } elsif (/ (\d{1,3} (?: \. \d{1,3}){3}) (?!\d) /x) {
- $ip = $1; last;
- } elsif (/ \[ (IPv6:)? ( ([0-9a-zA-Z]* : ){2,} [0-9a-zA-Z:.]* ) \] /xi) {
- $ip = $2; last;
- }
- }
- do_log(5, "fish_out_ip_from_received: $ip, $item");
- }
- !defined($ip) ? undef : $ip; # undef need not be tainted
- }
- # Splits unquoted fully qualified e-mail address, or an address
- # with missing domain part. Returns a pair: (localpart, domain).
- # The domain part (if nonempty) includes the '@' as the first character.
- # If the syntax is badly broken, everything ends up as the localpart.
- # The domain part can be an address literal, as specified by rfc2822.
- # Does not handle explicit route paths.
- #
- sub split_address($) {
- my($mailbox) = @_;
- $mailbox =~ /^ (.*?) ( \@ (?: \[ (?: \\. | [^\]\\] )* \]
- | [^@"<>\[\]\\\s] )*
- ) \z/xs ? ($1, $2) : ($mailbox, '');
- }
- # split_localpart() splits localpart of an e-mail address at the first
- # occurrence of the address extension delimiter character. (based on
- # equivalent routine in Postfix)
- #
- # Reserved addresses are not split: postmaster, mailer-daemon,
- # double-bounce. Addresses that begin with owner-, or addresses
- # that end in -request are not split when the owner_request_special
- # parameter is set.
- sub split_localpart($$) {
- my($localpart, $delimiter) = @_;
- my($owner_request_special) = 1; # configurable ???
- my($extension);
- if ($localpart =~ /^(postmaster|mailer-daemon|double-bounce)\z/i) {
- # do not split these, regardless of what the delimiter is
- } elsif ($delimiter eq '-' && $owner_request_special &&
- $localpart =~ /^owner-.|.-request\z/si) {
- # don't split owner-foo or foo-request
- } elsif ($localpart =~ /^(.+?)\Q$delimiter\E(.*)\z/s) {
- ($localpart, $extension) = ($1, $2);
- # do not split the address if the result would have a null localpart
- }
- ($localpart, $extension);
- }
- # For a given email address (e.g. for User+Foo@sub.exAMPLE.CoM)
- # prepare and return a list of lookup keys in the following order:
- # User+Foo@sub.exAMPLE.COM (as-is, no lowercasing)
- # user+foo@sub.example.com
- # user@sub.example.com (only if $recipient_delimiter nonempty)
- # user+foo(@) (only if $include_bare_user)
- # user(@) (only if $include_bare_user and $recipient_delimiter nonempty)
- # (@)sub.example.com
- # (@).sub.example.com
- # (@).example.com
- # (@).com
- # (@).
- # Note about (@): if $at_with_user is true the user-only keys (without domain)
- # get an '@' character appended (e.g. 'user+foo@'). Usual for lookup_hash.
- # If $at_with_user is false the domain-only (without localpart) keys
- # get a '@' prepended (e.g. '@.example.com'). Usual for SQL and LDAP lookups.
- #
- # The domain part is lowercased in all but the first item in the resulting
- # list; the localpart is lowercased iff $localpart_is_case_sensitive is true.
- #
- sub make_query_keys($$$) {
- my($addr,$at_with_user,$include_bare_user) = @_;
- my($localpart,$domain) = split_address($addr); $domain = lc($domain);
- my($saved_full_localpart) = $localpart;
- $localpart = lc($localpart) if !c('localpart_is_case_sensitive');
- # chop off leading @, and trailing dots
- $domain = $1 if $domain =~ /^\@?(.*?)\.*\z/s;
- my($extension); my($delim) = c('recipient_delimiter');
- if ($delim ne '') {
- ($localpart,$extension) = split_localpart($localpart,$delim);
- }
- $extension = '' if !defined($extension); # mute warnings
- my($append_to_user,$prepend_to_domain) = $at_with_user ? ('@','') : ('','@');
- my(@keys); # a list of query keys
- push(@keys, $addr); # as is
- push(@keys, $localpart.$delim.$extension.'@'.$domain)
- if $extension ne ''; # user+foo@example.com
- push(@keys, $localpart.'@'.$domain); # user@example.com
- if ($include_bare_user) { # typically enabled for local users only
- push(@keys, $localpart.$delim.$extension.$append_to_user)
- if $extension ne ''; # user+foo(@)
- push(@keys, $localpart.$append_to_user); # user(@)
- }
- push(@keys, $prepend_to_domain.$domain); # (@)sub.example.com
- if ($domain =~ /\[/) { # don't split address literals
- push(@keys, $prepend_to_domain.'.'); # (@).
- } else {
- my(@dkeys); my($d) = $domain;
- for (;;) { # (@).sub.example.com (@).example.com (@).com (@).
- push(@dkeys, $prepend_to_domain.'.'.$d);
- last if $d eq '';
- $d = ($d =~ /^([^.]*)\.(.*)\z/s) ? $2 : '';
- }
- if (@dkeys > 10) { @dkeys = @dkeys[$#dkeys-9 .. $#dkeys] } # sanity limit
- push(@keys,@dkeys);
- }
- my($keys_ref) = []; # remove duplicates
- for my $k (@keys) { push(@$keys_ref,$k) if !grep {$k eq $_} @$keys_ref }
- ll(5) && do_log(5,"query_keys: ".join(', ',@$keys_ref));
- # the rhs replacement strings are similar to what would be obtained
- # by lookup_re() given the following regular expression:
- # /^( ( ( [^@]*? ) ( \Q$delim\E [^@]* )? ) (?: \@ (.*) ) )$/xs
- my($rhs) = [ # a list of right-hand side replacement strings
- $addr, # $1 = User+Foo@Sub.Example.COM
- $saved_full_localpart, # $2 = User+Foo
- $localpart, # $3 = user
- $delim.$extension, # $4 = +foo
- $domain, # $5 = sub.example.com
- ];
- ($keys_ref, $rhs);
- }
- # quote_rfc2821_local() quotes the local part of a mailbox address
- # (given in internal (unquoted) form), and returns external (quoted)
- # mailbox address, as per rfc2821.
- #
- # Internal (unquoted) form is used internally by amavisd-new and other mail sw,
- # external (quoted) form is used in SMTP commands and message headers.
- #
- # The quote_rfc2821_local() conversion is necessary because addresses
- # we get from certain MTAs are raw, with stripped-off quoting.
- # To re-insert message back via SMTP, the local-part of the address needs
- # to be quoted again if it contains reserved characters or otherwise
- # does not obey the dot-atom syntax, as specified in rfc2821.
- # Failing to do that gets us into trouble: amavis accepts message from MTA,
- # but is unable to hand it back to MTA after checking, receiving
- # '501 Bad address syntax' with every attempt.
- #
- sub quote_rfc2821_local($) {
- my($mailbox) = @_;
- # atext: any character except controls, SP, and specials (rfc2821/rfc2822)
- my($atext) = "a-zA-Z0-9!#\$%&'*/=?^_`{|}~+-";
- # my($specials) = '()<>\[\]\\\\@:;,."';
- my($localpart,$domain) = split_address($mailbox);
- if ($localpart !~ /^[$atext]+(\.[$atext]+)*\z/so) { # not dot-atom
- $localpart =~ s/(["\\])/\\$1/g; # quoted-pair
- # special case: Postfix hates ""@domain but is not so harsh on @domain
- $localpart = '"'.$localpart.'"' if $localpart ne ''; # make it a qcontent
- }
- $domain = '' if $domain eq '@'; # strip off empty domain entirely
- $localpart . $domain;
- }
- # wraps the result of quote_rfc2821_local into angle brackets <...> ;
- # If given a list, it returns a list (possibly converted to
- # comma-separated scalar if invoked in scalar context), quoting each element;
- #
- sub qquote_rfc2821_local(@) {
- my(@r) = map { $_ eq '' ? '<>' : ('<' . quote_rfc2821_local($_) . '>') } @_;
- wantarray ? @r : join(', ', @r);
- }
- # unquote_rfc2821_local() strips away the quoting from the local part
- # of an external (quoted) mailbox address, and returns internal (unquoted)
- # mailbox address, as per rfc2821.
- #
- # Internal (unquoted) form is used internally by amavisd-new and other mail sw,
- # external (quoted) form is used in SMTP commands and message headers.
- #
- sub unquote_rfc2821_local($) {
- my($mailbox) = @_;
- # the angle-bracket stripping is not really a duty of this subroutine,
- # as it should have been already done elsewhere, but for the time being
- # we do it here:
- $mailbox = $1 if $mailbox =~ /^ \s* < ( .* ) > \s* \z/xs;
- my($localpart,$domain) = split_address($mailbox);
- $localpart =~ s/ " | \\ (.) | \\ \z /$1/xsg; # unquote quoted-pairs
- $localpart . $domain;
- }
- # Prepare a single SMTP response and an exit status as per sysexits.h
- # from individual per-recipient response codes, taking into account
- # sendmail milter specifics. Returns a triple: (smtp response, exit status,
- # an indication whether DSN is needed).
- #
- sub one_response_for_all($$$) {
- my($msginfo, $dsn_per_recip_capable, $am_id) = @_;
- my($smtp_resp, $exit_code, $dsn_needed);
- my($delivery_method) = $msginfo->delivery_method;
- my($sender) = $msginfo->sender;
- my($per_recip_data) = $msginfo->per_recip_data;
- my($any_not_done) = scalar(grep { !$_->recip_done } @$per_recip_data);
- if ($delivery_method ne '' && $any_not_done)
- { die "Explicit forwarding, but not all recips done" }
- if (!@$per_recip_data) { # no recipients, nothing to do
- $smtp_resp = "250 2.5.0 Ok, id=$am_id"; $exit_code = EX_OK;
- do_log(5, "one_response_for_all <$sender>: no recipients, '$smtp_resp'");
- }
- if (!defined $smtp_resp) {
- for my $r (@$per_recip_data) { # any 4xx code ?
- if ($r->recip_smtp_response =~ /^4/) # pick the first 4xx code
- { $smtp_resp = $r->recip_smtp_response; last }
- }
- if (!defined $smtp_resp) {
- for my $r (@$per_recip_data) { # any invalid code ?
- if ($r->recip_done && $r->recip_smtp_response !~ /^[245]/) {
- $smtp_resp = '451 4.5.0 Bad SMTP response code??? "'
- . $r->recip_smtp_response . '"';
- last; # pick the first
- }
- }
- }
- if (defined $smtp_resp) {
- $exit_code = EX_TEMPFAIL;
- do_log(5, "one_response_for_all <$sender>: 4xx found, '$smtp_resp'");
- }
- }
- # NOTE: a 2xx SMTP response code is set both by internal Discard
- # and by a genuine successful delivery. To distinguish between the two
- # we need to check $r->recip_destiny as well.
- #
- if (!defined $smtp_resp) {
- # if destiny for _all_ recipients is D_DISCARD, give Discard
- my($notall);
- for my $r (@$per_recip_data) {
- if ($r->recip_destiny == D_DISCARD) # pick the first DISCARD code
- { $smtp_resp = $r->recip_smtp_response if !defined $smtp_resp }
- else { $notall++; last } # one is not a discard, nogood
- }
- if ($notall) { $smtp_resp = undef }
- if (defined $smtp_resp) {
- # helper program will interpret 99 as discard
- $exit_code = $delivery_method eq '' ? 99 : EX_OK;
- do_log(5, "one_response_for_all <$sender>: all DISCARD, '$smtp_resp'");
- }
- }
- if (!defined $smtp_resp) {
- # destiny for _all_ recipients is Discard or Reject, give 5xx
- # (and there is at least one Reject)
- my($notall, $done_level);
- my($bounce_cnt) = 0;
- for my $r (@$per_recip_data) {
- my($dest, $resp) = ($r->recip_destiny, $r->recip_smtp_response);
- if ($dest == D_DISCARD) {
- # ok, this one is discard, let's see the rest
- } elsif ($resp =~ /^5/ && $dest != D_BOUNCE) {
- # prefer to report SMTP response code of genuine rejects
- # from MTA, over internal rejects by content filters
- if (!defined $smtp_resp || $r->recip_done > $done_level)
- { $smtp_resp = $resp; $done_level = $r->recip_done }
- } else { $notall++; last } # one is Pass or Bounce, nogood
- }
- if ($notall) { $smtp_resp = undef }
- if (defined $smtp_resp) {
- $exit_code = EX_UNAVAILABLE;
- do_log(5, "one_response_for_all <$sender>: REJECTs, '$smtp_resp'");
- }
- }
- if (!defined $smtp_resp) {
- # mixed destiny => 2xx, but generate dsn for bounces and rejects
- my($rej_cnt) = 0; my($bounce_cnt) = 0; my($drop_cnt) = 0;
- for my $r (@$per_recip_data) {
- my($dest, $resp) = ($r->recip_destiny, $r->recip_smtp_response);
- if ($resp =~ /^2/ && $dest == D_PASS) # genuine successful delivery
- { $smtp_resp = $resp if !defined $smtp_resp }
- $drop_cnt++ if $dest == D_DISCARD;
- if ($resp =~ /^5/)
- { if ($dest == D_BOUNCE) { $bounce_cnt++ } else { $rej_cnt++ } }
- }
- $exit_code = EX_OK;
- if (!defined $smtp_resp) { # no genuine Pass/2xx
- # declare success, we'll handle bounce
- $smtp_resp = "250 2.5.0 Ok, id=$am_id";
- if ($any_not_done) { $smtp_resp .= ", continue delivery" }
- elsif ($delivery_method eq '') { $exit_code = 99 } # milter DISCARD
- }
- if ($rej_cnt + $bounce_cnt + $drop_cnt > 0) {
- $smtp_resp .= ", ";
- $smtp_resp .= "but " if $rej_cnt+$bounce_cnt+$drop_cnt<@$per_recip_data;
- $smtp_resp .= join ", and ",
- map { my($cnt, $nm) = @$_;
- !$cnt ? () : $cnt == @$per_recip_data ? $nm : "$cnt $nm"
- } ([$rej_cnt,'REJECT'], [$bounce_cnt,'BOUNCE'], [$drop_cnt,'DISCARD']);
- }
- $dsn_needed =
- ($bounce_cnt > 0 || ($rej_cnt > 0 && !$dsn_per_recip_capable)) ? 1 : 0;
- ll(5) && do_log(5,"one_response_for_all <$sender>: "
- . ($rej_cnt + $bounce_cnt + $drop_cnt > 0 ? 'mixed' : 'success')
- . ", r=$rej_cnt,b=$bounce_cnt,d=$drop_cnt"
- . ", dsn_needed=$dsn_needed, '$smtp_resp'");
- }
- ($smtp_resp, $exit_code, $dsn_needed);
- }
- 1;
- #
- package Amavis::Lookup::RE;
- use strict;
- use re 'taint';
- BEGIN {
- use Exporter ();
- use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.043';
- @ISA = qw(Exporter);
- }
- BEGIN { import Amavis::Util qw(ll do_log fmt_struct) }
- # Make an object out of the supplied lookup list
- # to make it distinguishable from simple ACL array
- sub new($$) { my($class) = shift; bless [@_], $class }
- # lookup_re() performs a lookup for an e-mail address or other key string
- # against a list made up of regular expressions.
- #
- # A full unmodified e-mail address is always used, so splitting to localpart
- # and domain or lowercasing is NOT performed. The regexp is powerful enough
- # that this can be accomplished by its mechanisms. The routine is useful for
- # other RE tests besides the usual e-mail addresses, such as looking for
- # banned file names.
- #
- # Each element of the list can be ref to a pair, or directly a regexp
- # ('Regexp' object created by a qr operator, or just a (less efficient)
- # string containing a regular expression). If it is a pair, the first
- # element is treated as a regexp, and the second provides a value in case
- # the regexp matches. If not a pair, the implied result of a match is 1.
- #
- # The regular expression is taken as-is, no implicit anchoring or setting
- # case insensitivity is done, so do use a qr'(?i)^user@example\.com$',
- # and not a sloppy qr'user@example.com', which can easily backfire.
- # Also, if qr is used with a delimiter other than ' (apostrophe), make sure
- # to quote the @ and $ .
- #
- # The pattern allows for capturing of parenthesized substrings, which can
- # then be referenced from the result string using the $1, $2, ... notation,
- # as with the Perl m// operator. The number after a $ may be a multi-digit
- # decimal number. To avoid possible ambiguity the ${n} or $(n) form may be used
- # Substring numbering starts with 1. Nonexistent references evaluate to empty
- # strings. If any substitution is done, the result inherits the taintedness
- # of $addr. Keep in mind that $ and @ characters needs to be backslash-quoted
- # in qq() strings. Example:
- # $virus_quarantine_to = new_RE(
- # [ qr'^(.*)@example\.com$'i => 'virus-${1}@example.com' ],
- # [ qr'^(.*)(@[^@]*)?$'i => 'virus-${1}${2}' ] );
- #
- # Example (equivalent to the example in lookup_acl):
- # $acl_re = Amavis::Lookup::RE->new(
- # qr'@me\.ac\.uk$'i, [qr'[@.]ac\.uk$'i=>0], qr'\.uk$'i );
- # ($r,$k) = $acl_re->lookup_re('user@me.ac.uk');
- # or $r = lookup(0, 'user@me.ac.uk', $acl_re);
- #
- # 'user@me.ac.uk' matches me.ac.uk, returns true and search stops
- # 'user@you.ac.uk' matches .ac.uk, returns false (because of =>0) and search stops
- # 'user@them.co.uk' matches .uk, returns true and search stops
- # 'user@some.com' does not match anything, falls through and returns false (undef)
- #
- # As a special allowance, the $addr argument may be a ref to a list of search
- # keys. At each step in traversing the supplied regexp list, all elements of
- # @$addr are tried. If any of them matches, the search stops. This is currently
- # used in banned names lookups, where all attributes of a part are given as a
- # list @$addr.
- sub lookup_re($$;$) {
- my($self, $addr,$get_all) = @_;
- local($1,$2,$3,$4); my(@matchingkey,@result);
- for my $e (@$self) { # try each regexp in the list
- my($key,$r);
- if (ref($e) eq 'ARRAY') { # a pair: (regexp,result)
- ($key,$r) = ($e->[0], @$e < 2 ? 1 : $e->[1]);
- } else { # a single regexp (not a pair), implies result 1
- ($key,$r) = ($e, 1);
- }
- ""=~/x{0}/; # braindead Perl: serves as explicit deflt for an empty regexp
- my(@rhs); # match, capturing parenthesized subpatterns in @rhs
- if (!ref($addr)) { @rhs = $addr =~ /$key/ }
- else { for (@$addr) { @rhs = /$key/; last if @rhs } }
- if (@rhs) { # regexp matches
- # do the righthand side replacements if any $n, ${n} or $(n) is specified
- if (!ref($r) && $r=~/\$/) {
- my($any) = $r =~ s{ \$ ( (\d+) | \{ (\d+) \} | \( (\d+) \) ) }
- { my($j)=$2+$3+$4; $j<1 ? '' : $rhs[$j-1] }gxse;
- # bring taintedness of input to the result
- $r .= substr($addr,0,0) if $any;
- }
- push(@result,$r); push(@matchingkey,$key);
- last if !$get_all;
- }
- }
- if (!ll(5)) {
- # don't bother preparing log report which will not be printed
- } elsif (!@result) {
- do_log(5,sprintf("lookup_re(%s), no matches", fmt_struct($addr)));
- } else { # pretty logging
- my(%esc) = (r => "\r", n => "\n", f => "\f", b => "\b",
- e => "\e", a => "\a", t => "\t");
- my(@mk) = @matchingkey;
- for my $mk (@mk) # undo the \-quoting, will be redone by logging routines
- { $mk =~ s{ \\(.) }{ exists($esc{$1}) ? $esc{$1} : $1 }egsx }
- if (!$get_all) { # first match wins
- do_log(5,sprintf('lookup_re(%s) matches key "%s", result=%s',
- fmt_struct($addr), $mk[0], fmt_struct($result[0])));
- } else { # want all matches
- do_log(5,sprintf("lookup_re(%s) matches keys: %s", fmt_struct($addr),
- join(', ', map {sprintf('"%s"=>%s', $mk[$_],fmt_struct($result[$_]))}
- (0..$#result))));
- }
- }
- if (!$get_all) { !wantarray ? $result[0] : ($result[0], $matchingkey[0]) }
- else { !wantarray ? \@result : (\@result, \@matchingkey) }
- }
- 1;
- #
- package Amavis::Lookup::IP;
- use strict;
- use re 'taint';
- BEGIN {
- use Exporter ();
- use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.043';
- @ISA = qw(Exporter);
- @EXPORT_OK = qw(&lookup_ip_acl);
- }
- use subs @EXPORT_OK;
- BEGIN {
- import Amavis::Util qw(ll do_log);
- }
- # ip_to_vec() takes IPv6 or IPv4 IP address with optional prefix length
- # (or IPv4 mask), parses and validates it, and returns it as a 128-bit
- # vector string that can be used as operand to Perl bitwise string operators.
- # Syntax and other errors in the argument throw exception (die).
- # If the second argument $allow_mask is 0, the prefix length or mask
- # specification is not allowed as part of the IP address.
- #
- # The IPv6 syntax parsing and validation adheres to rfc3513.
- # All the following IPv6 address forms are supported:
- # x:x:x:x:x:x:x:x preferred form
- # x:x:x:x:x:x:d.d.d.d alternative form
- # ...::... zero-compressed form
- # addr/prefix-length prefix length may be specified (defaults to 128)
- # Optionally an "IPv6:" prefix may be prepended to the IPv6 address
- # as specified by rfc2821. Brackets enclosing the address are allowed
- # for Postfix compatibility, e.g. [::1]/128 .
- #
- # The following IPv4 forms are allowed:
- # d.d.d.d
- # d.d.d.d/prefix-length CIDR mask length is allowed (defaults to 32)
- # d.d.d.d/m.m.m.m network mask (gets converted to prefix-length)
- # If prefix-length or a mask is specified with an IPv4 address, the address
- # may be shortened to d.d.d/n or d.d/n or d/n. Such truncation is allowed
- # for compatibility with earlier version, but is deprecated and is not
- # allowed for IPv6 addresses.
- #
- # IPv4 addresses and masks are converted to IPv4-mapped IPv6 addresses
- # of the form ::FFFF:d.d.d.d, The CIDR mask length (0..32) is converted
- # to IPv6 prefix-length (96..128). The returned vector strings resulting
- # from IPv4 and IPv6 forms are indistinguishable.
- #
- # NOTE:
- # d.d.d.d is equivalent to ::FFFF:d.d.d.d (IPv4-mapped IPv6 address)
- # which is not the same as ::d.d.d.d (IPv4-compatible IPv6 address)
- #
- # A triple is returned:
- # - IP address represented as a 128-bit vector (a string)
- # - network mask derived from prefix length, a 128-bit vector (string)
- # - prefix length as an integer (0..128)
- #
- sub ip_to_vec($;$) {
- my($ip,$allow_mask) = @_;
- my($ip_len); my(@ip_fields);
- local($1,$2,$3,$4,$5,$6);
- $ip =~ s/^[ \t]+//; $ip =~ s/[ \t\n]+\z//s; # trim
- my($ipa) = $ip;
- ($ipa,$ip_len) = ($1,$2) if $allow_mask && $ip =~ m{^([^/]*)/(.*)\z}s;
- $ipa = $1 if $ipa =~ m{^ \[ (.*) \] \z}xs; # discard optional brackets
- $ipa = $1 if $ipa =~ m{^(.*)%[A-Za-z0-9]+\z}s; # discard interface spec
- if ($ipa =~ m{^(IPv6:)?(.*:)(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\z}si){
- # IPv6 alternative form x:x:x:x:x:x:d.d.d.d
- my(@d) = ($3,$4,$5,$6);
- !grep {$_ > 255} @d
- or die "Invalid decimal field value in IPv6 address: [$ip]\n";
- $ipa = $2 . sprintf("%02X%02X:%02X%02X", @d);
- } elsif ($ipa =~ m{^\d{1,3}(?:\.\d{1,3}){0,3}\z}) { # IPv4 form
- my(@d) = split(/\./,$ipa,-1);
- !grep {$_ > 255} @d
- or die "Invalid field value in IPv4 address: [$ip]\n";
- defined($ip_len) || @d==4
- or die "IPv4 address [$ip] contains fewer than 4 fields\n";
- $ipa = '::FFFF:' . sprintf("%02X%02X:%02X%02X", @d); # IPv4-mapped IPv6
- if (!defined($ip_len)) { $ip_len = 32; # no length, defaults to /32
- } elsif ($ip_len =~ /^\d{1,9}\z/) { # /n, IPv4 CIDR notation
- } elsif ($ip_len =~ /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\z/) {
- !grep {$_ > 255} ($1,$2,$3,$4)
- or die "Illegal field value in IPv4 mask: [$ip]\n";
- my($mask1) = pack('C4',$1,$2,$3,$4); # /m.m.m.m
- my($len) = unpack("%b*",$mask1); # count ones
- my($mask2) = pack('B32', '1' x $len); # reconstruct mask from count
- $mask1 eq $mask2
- or die "IPv4 mask not representing valid CIDR mask: [$ip]\n";
- $ip_len = $len;
- } else {
- die "Invalid IPv4 network mask or CIDR prefix length: [$ip]\n";
- }
- $ip_len<=32 or die "IPv4 network prefix length greater than 32: [$ip]\n";
- $ip_len += 128-32; # convert IPv4 net mask length to IPv6 prefix length
- }
- $ip_len = 128 if !defined($ip_len);
- $ip_len<=128 or die "IPv6 network prefix length greater than 128: [$ip]\n";
- $ipa =~ s/^IPv6://i;
- # now we presumably have an IPv6 preferred form x:x:x:x:x:x:x:x
- if ($ipa !~ /^(.*?)::(.*)\z/s) { # zero-compressing form used?
- @ip_fields = split(/:/,$ipa,-1); # no
- } else { # expand zero-compressing form
- my(@a) = split(/:/,$1,-1); my(@b) = split(/:/,$2,-1);
- my($missing_cnt) = 8-(@a+@b); $missing_cnt = 1 if $missing_cnt<1;
- @ip_fields = (@a, (0) x $missing_cnt, @b);
- }
- !grep { !/^[0-9a-zA-Z]{1,4}\z/ } @ip_fields # this is quite slow
- or die "Invalid syntax of IPv6 address: [$ip]\n";
- @ip_fields<8 and die "IPv6 address [$ip] contains fewer than 8 fields\n";
- @ip_fields>8 and die "IPv6 address [$ip] contains more than 8 fields\n";
- my($vec) = pack("n8", map {hex} @ip_fields);
- $ip_len=~/^\d{1,3}\z/
- or die "Invalid prefix length syntax in IP address: [$ip]\n";
- $ip_len<=128 or die "Invalid prefix length in IPv6 address: [$ip]\n";
- my($mask) = pack('B128', '1' x $ip_len);
- # do_log(5,sprintf("ip_to_vec: %s => %s/%d\n", $ip,unpack("B*",$vec),$ip_len));
- ($vec,$mask,$ip_len);
- }
- # lookup_ip_acl() performs a lookup for an IPv4 or IPv6 address
- # against access control list or a hash of network or host addresses.
- #
- # IP address is compared to each member of an access list in turn,
- # the first match wins (terminates the search), and its value decides
- # whether the result is true (yes, permit, pass) or false (no, deny, drop).
- # Falling through without a match produces false (undef).
- #
- # The presence of character '!' prepended to a list member decides
- # whether the result will be true (without a '!') or false (with '!')
- # in case this list member matches and terminates the search.
- #
- # Because search stops at the first match, it only makes sense
- # to place more specific patterns before the more general ones.
- #
- # For IPv4 a network address can be specified in classless notation
- # n.n.n.n/k, or using a mask n.n.n.n/m.m.m.m . Missing mask implies /32,
- # i.e. a host address. For IPv6 addresses all rfc3513 forms are allowed.
- # See also comments at ip_to_vec().
- #
- # Although not a special case, it is good to remember that '::/0'
- # always matches any IPv4 or IPv6 address (even syntactically invalid address).
- #
- # The '0/0' is equivalent to '::FFFF:0:0/96' and matches any syntactically
- # valid IPv4 address (including IPv4-mapped IPv6 addresses), but not other
- # IPv6 addresses!
- #
- # Example
- # given: @acl = qw( !192.168.1.12 172.16.3.3 !172.16.3.0/255.255.255.0
- # 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16
- # !0.0.0.0/8 !:: 127.0.0.0/8 ::1 );
- # matches rfc1918 private address space except host 192.168.1.12
- # and net 172.16.3/24 (but host 172.16.3.3 within 172.16.3/24 still matches).
- # In addition, the 'unspecified' (null, i.e. all zeros) IPv4 and IPv6
- # addresses return false, and IPv4 and IPv6 loopback addresses match
- # and return true.
- #
- # If the supplied lookup table is a hash reference, match a canonical IP
- # address: dot-quad IPv4, or preferred IPv6 form, against hash keys. For IPv4
- # addresses a simple classful subnet specification is allowed in hash keys
- # by truncating trailing bytes from the looked up IPv4 address. A syntactically
- # invalid IP address can only match a hash entry with an undef key.
- #
- sub lookup_ip_acl($@) {
- my($ip, @nets_ref) = @_;
- my($ip_vec,$ip_mask) = eval { ip_to_vec($ip,0) }; my($eval_stat) = $@;
- my($label,$fullkey,$result); my($found) = 0;
- for my $tb (@nets_ref) {
- my($t) = ref($tb) eq 'REF' ? $$tb : $tb; # allow one level of indirection
- if (!ref($t) || ref($t) eq 'SCALAR') { # a scalar always matches
- my($r) = ref($t) ? $$t : $t; # allow direct or indirect reference
- $result = $r; $fullkey = "(constant:$r)";
- $found++ if defined $result;
- } elsif (ref($t) eq 'HASH') {
- if (!defined $ip_vec) { # syntactically invalid IP address
- $fullkey = undef; $result = $t->{$fullkey};
- $found++ if defined $result;
- } else { # valid IP address
- # match the canonical IP address: dot-quad IPv4, or preferred IPv6 form
- my($ip_c); # IP address in the canonical form: x:x:x:x:x:x:x:x
- my($ip_dq); # IPv4 in a dotted-quad form if IPv4-mapped, or undef
- $ip_c = join(':', map {sprintf('%04x',$_)} unpack('n8',$ip_vec));
- my($ipv4_vec,$ipv4_mask) = ip_to_vec('::FFFF:0:0/96',1);
- if ( ($ip_vec & $ipv4_mask) eq ($ipv4_vec & $ipv4_mask) ) {
- # is an IPv4-mapped IPv6 address, format it in a dot-quad form
- $ip_dq = join('.', unpack('C4',substr($ip_vec,12,4))); # last 32 bits
- }
- do_log(5, "lookup_ip_acl keys: \"$ip_dq\", \"$ip_c\"");
- if (defined $ip_dq) { # try dot-quad if applicable
- for (my(@f)=split(/\./,$ip_dq); @f && !$found; $#f--) {
- $fullkey = join('.',@f); $result = $t->{$fullkey};
- $found++ if defined $result;
- }
- }
- if (!$found) { # try the 'preferred IPv6 form'
- $fullkey = $ip_c; $result = $t->{$fullkey};
- $found++ if defined $result;
- }
- }
- } elsif (ref($t) eq 'ARRAY') {
- my($key, $acl_ip_vec, $acl_mask, $acl_mask_len);
- for my $net (@$t) {
- $fullkey = $key = $net; $result = 1;
- if ($key =~ /^(!+)(.*)\z/s) { # starts with exclamation mark(s)
- $key = $2;
- $result = 1 - $result if (length($1) & 1); # negate if odd
- }
- ($acl_ip_vec, $acl_mask, $acl_mask_len) = ip_to_vec($key,1);
- if ($acl_mask_len == 0) { $found++ } # even invalid address matches /0
- elsif (!defined($ip_vec)) {} # no other matches for invalid address
- elsif (($ip_vec & $acl_mask) eq ($acl_ip_vec & $acl_mask)) { $found++ }
- last if $found;
- }
- } elsif ($t->isa('Amavis::Lookup::IP')) { # pre-parsed IP lookup array obj
- my($acl_ip_vec, $acl_mask, $acl_mask_len);
- for my $e (@$t) {
- ($fullkey, $acl_ip_vec, $acl_mask, $acl_mask_len, $result) = @$e;
- if ($acl_mask_len == 0) { $found++ } # even invalid address matches /0
- elsif (!defined($ip_vec)) {} # no other matches for invalid address
- elsif (($ip_vec & $acl_mask) eq ($acl_ip_vec & $acl_mask)) { $found++ }
- last if $found;
- }
- } elsif ($t->isa('Amavis::Lookup::Label')) { # logging label
- # just a convenience for logging purposes, not a real lookup method
- $label = $t->display; # grab the name, and proceed with the next table
- } else {
- die "TROUBLE: lookup table is an unknown object: " . ref($t);
- }
- last if $found;
- }
- $fullkey = $result = undef if !$found;
- if ($label ne '') { $label = " ($label)" }
- ll(4) && do_log(4, "lookup_ip_acl$label: key=\"$ip\""
- . (!$found ? ", no match" : " matches \"$fullkey\", result=$result"));
- if ($eval_stat eq '') { $eval_stat = undef }
- else {
- chomp($eval_stat); $eval_stat = "lookup_ip_acl$label: $eval_stat";
- do_log(2, $eval_stat);
- }
- !wantarray ? $result : ($result, $fullkey, $eval_stat);
- }
- # create a pre-parsed object from a list of IP networks,
- # which may be used as an argument to lookup_ip_acl to speed up its searches
- sub new($@) {
- my($class,@nets) = @_;
- my(@list);
- for my $net (@nets) {
- my($key) = $net; my($result) = 1;
- if ($key =~ /^(!+)(.*)\z/s) { # starts with exclamation mark(s)
- $key = $2;
- $result = 1 - $result if (length($1) & 1); # negate if odd
- }
- my($ip_vec, $ip_mask, $ip_mask_len) = ip_to_vec($key,1);
- push(@list, [$net, $ip_vec, $ip_mask, $ip_mask_len, $result]);
- }
- bless \@list, $class;
- }
- 1;
- #
- package Amavis::Lookup::Label;
- use strict;
- use re 'taint';
- # Make an object out of the supplied string, to serve as label
- # in log messages generated by sub lookup
- sub new($$) { my($class) = shift; my($str) = shift; bless \$str, $class }
- sub display($) { my($self) = shift; $$self }
- 1;
- #
- package Amavis::Lookup;
- use strict;
- use re 'taint';
- BEGIN {
- use Exporter ();
- use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.043';
- @ISA = qw(Exporter);
- @EXPORT_OK = qw(&lookup);
- }
- use subs @EXPORT_OK;
- BEGIN {
- import Amavis::Util qw(ll do_log fmt_struct);
- import Amavis::Conf qw(:platform c cr ca);
- import Amavis::Timing qw(section_time);
- import Amavis::rfc2821_2822_Tools qw(split_address make_query_keys);
- }
- # lookup_hash() performs a lookup for an e-mail address against a hash map.
- # If a match is found (a hash key exists in the Perl hash) the function returns
- # whatever the map returns, otherwise undef is returned. First match wins,
- # aborting further search sequence.
- #
- sub lookup_hash($$;$) {
- my($addr, $hash_ref,$get_all) = @_;
- (ref($hash_ref) eq 'HASH')
- or die "lookup_hash: arg2 must be a hash ref: $hash_ref";
- local($1,$2,$3,$4); my(@matchingkey,@result);
- my($keys_ref,$rhs_ref) = make_query_keys($addr,1,1);
- for my $key (@$keys_ref) { # do the search
- if (exists $$hash_ref{$key}) { # got it
- push(@result,$$hash_ref{$key}); push(@matchingkey,$key);
- last if !$get_all;
- }
- }
- # do the right-hand side replacements if any $n, ${n} or $(n) is specified
- for my $r (@result) { # remember that $r is just an alias to array elements
- if (!ref($r) && $r=~/\$/) { # is a plain string containing a '$'
- my($any) = $r =~ s{ \$ ( (\d+) | \{ (\d+) \} | \( (\d+) \) ) }
- { my($j)=$2+$3+$4; $j<1 ? '' : $rhs_ref->[$j-1] }gxse;
- # bring taintedness of input to the result
- $r .= substr($addr,0,0) if $any;
- }
- }
- if (!ll(5)) {
- # only bother with logging when needed
- } elsif (!@result) {
- do_log(5,"lookup_hash($addr), no matches");
- } elsif (!$get_all) { # first match wins
- do_log(5,sprintf('lookup_hash(%s) matches key "%s", result=%s',
- $addr,$matchingkey[0],$result[0]));
- } else { # want all matches
- do_log(5,"lookup_hash($addr) matches keys: ".
- join(', ', map {sprintf('"%s"=>%s',$matchingkey[$_],$result[$_])}
- (0..$#result)));
- }
- if (!$get_all) { !wantarray ? $result[0] : ($result[0], $matchingkey[0]) }
- else { !wantarray ? \@result : (\@result, \@matchingkey) }
- }
- # lookup_acl() performs a lookup for an e-mail address against
- # access control list.
- #
- # The supplied e-mail address is compared with each member of the
- # lookup list in turn, the first match wins (terminates the search),
- # and its value decides whether the result is true (yes, permit, pass)
- # or false (no, deny, drop). Falling through without a match
- # produces false (undef). Search is case-insensitive.
- #
- # lookup_acl is not aware of address extensions and they are not
- # handled specially.
- #
- # If a list element contains a '@', the full e-mail address is compared,
- # otherwise if a list element has a leading dot, the domain name part is
- # matched only, and the domain as well as its subdomains can match. If there
- # is no leading dot, the domain must match exactly (subdomains do not match).
- #
- # The presence of character '!' prepended to a list element decides
- # whether the result will be true (without a '!') or false (with '!')
- # in case this list element matches and terminates the search.
- #
- # Because search stops at the first match, it only makes sense
- # to place more specific patterns before the more general ones.
- #
- # Although not a special case, it is good to remember that '.' always matches,
- # so a '.' would stop the search and return true, whereas '!.' would stop the
- # search and return false (0).
- #
- # Examples:
- #
- # given: @acl = qw( me.ac.uk !.ac.uk .uk )
- # 'me.ac.uk' matches me.ac.uk, returns true and search stops
- #
- # given: @acl = qw( me.ac.uk !.ac.uk .uk )
- # 'you.ac.uk' matches .ac.uk, returns false (because of '!') and search stops
- #
- # given: @acl = qw( me.ac.uk !.ac.uk .uk )
- # 'them.co.uk' matches .uk, returns true and search stops
- #
- # given: @acl = qw( me.ac.uk !.ac.uk .uk )
- # 'some.com' does not match anything, falls through and returns false (undef)
- #
- # given: @acl = qw( me.ac.uk !.ac.uk .uk !. )
- # 'some.com' similar to previous, except it returns 0 instead of undef,
- # which would only make a difference if this ACL is not the last argument
- # in a call to lookup()
- #
- # given: @acl = qw( me.ac.uk !.ac.uk .uk . )
- # 'some.com' matches catchall ".", and returns true. The ".uk" is redundant
- #
- # more complex example: @acl = qw(
- # !The.Boss@dept1.xxx.com .dept1.xxx.com
- # .dept2.xxx.com .dept3.xxx.com lab.dept4.xxx.com
- # sub.xxx.com !.sub.xxx.com
- # me.d.aaa.com him.d.aaa.com !.d.aaa.com .aaa.com
- # );
- sub lookup_acl($$) {
- my($addr, $acl_ref) = @_;
- (ref($acl_ref) eq 'ARRAY')
- or die "lookup_acl: arg2 must be a list ref: $acl_ref";
- return undef if !@$acl_ref; # empty list can't match anything
- my($lpcs) = c('localpart_is_case_sensitive');
- my($localpart,$domain) = split_address($addr); $domain = lc($domain);
- $localpart = lc($localpart) if !$lpcs;
- local($1,$2);
- # chop off leading @ and trailing dots
- $domain = $1 if $domain =~ /^\@?(.*?)\.*\z/s;
- my($lcaddr) = $localpart . '@' . $domain;
- my($matchingkey, $result); my($found) = 0;
- for my $e (@$acl_ref) {
- $result = 1; $matchingkey = $e; my($key) = $e;
- if ($key =~ /^(!+)(.*)\z/s) { # starts with an exclamation mark(s)
- $key = $2;
- $result = 1-$result if (length($1) & 1); # negate if odd
- }
- if ($key =~ /^(.*?)\@([^@]*)\z/s) { # contains '@', check full address
- $found++ if $localpart eq ($lpcs?$1:lc($1)) && $domain eq lc($2);
- } elsif ($key =~ /^\.(.*)\z/s) { # leading dot: domain or subdomain
- my($key_t) = lc($1);
- $found++ if $domain eq $key_t || $domain =~ /(\.|\z)\Q$key_t\E\z/s;
- } else { # match domain (but not its subdomains)
- $found++ if $domain eq lc($key);
- }
- last if $found;
- }
- $matchingkey = $result = undef if !$found;
- do_log(5, "lookup_acl($addr)".
- (!$found?", no match":" matches key \"$matchingkey\", result=$result"));
- !wantarray ? $result : ($result, $matchingkey);
- }
- # Perform a lookup for an e-mail address against any number of supplied maps:
- # - SQL map,
- # - LDAP map,
- # - hash map (associative array),
- # - (access control) list,
- # - a list of regular expressions (an Amavis::Lookup::RE object),
- # - a (defined) scalar always matches, and returns itself as the 'map' value
- # (useful as a catchall for final 'pass' or 'fail');
- # (see lookup_hash, lookup_acl, lookup_sql and lookup_ldap for details).
- #
- # when $get_all is 0 (the common usage):
- # If a match is found (a defined value), returns whatever the map returns,
- # otherwise returns undef. FIRST match aborts further search sequence.
- # when $get_all is true:
- # Collects a list of results from ALL matching tables, and within each
- # table from ALL matching key. Returns a ref to the a list of results
- # (and a ref to a list of matching keys if returning a pair).
- # The first element of both lists is supposed to be what lookup() would
- # have returned if $get_all were 0. The order of returned elements
- # corresponds to the order of the search.
- #
- sub lookup($$@) {
- my($get_all, $addr, @tables) = @_;
- my($label, @result,@matchingkey);
- for my $tb (@tables) {
- my($t) = ref($tb) eq 'REF' ? $$tb : $tb; # allow one level of indirection
- if (!ref($t) || ref($t) eq 'SCALAR') { # a scalar always matches
- my($r) = ref($t) ? $$t : $t; # allow direct or indirect reference
- if (defined $r) {
- do_log(5,"lookup: (scalar) matches, result=\"$r\"");
- push(@result,$r); push(@matchingkey,"(constant:$r)");
- }
- } elsif (ref($t) eq 'HASH') {
- my($r,$mk) = lookup_hash($addr,$t,$get_all);
- if (!defined $r) {}
- elsif (!$get_all) { push(@result,$r); push(@matchingkey,$mk) }
- elsif (@$r) { push(@result,@$r); push(@matchingkey,@$mk) }
- } elsif (ref($t) eq 'ARRAY') {
- my($r,$mk) = lookup_acl($addr,$t);
- if (defined $r) { push(@result,$r); push(@matchingkey,$mk) }
- } elsif ($t->isa('Amavis::Lookup::Label')) { # logging label
- # just a convenience for logging purposes, not a real lookup method
- $label = $t->display; # grab the name, and proceed with the next table
- } elsif ($t->isa('Amavis::Lookup::RE')) {
- my($r,$mk) = $t->lookup_re($addr,$get_all);
- if (!defined $r) {}
- elsif (!$get_all) { push(@result,$r); push(@matchingkey,$mk) }
- elsif (@$r) { push(@result,@$r); push(@matchingkey,@$mk) }
- } elsif ($t->isa('Amavis::Lookup::SQL')) {
- my($r,$mk) = $t->lookup_sql($addr,$get_all);
- if (!defined $r) {}
- elsif (!$get_all) { push(@result,$r); push(@matchingkey,$mk) }
- elsif (@$r) { push(@result,@$r); push(@matchingkey,@$mk) }
- } elsif ($t->isa('Amavis::Lookup::SQLfield')) {
- my($r,$mk) = $t->lookup_sql_field($addr,$get_all);
- if (!defined $r) {}
- elsif (!$get_all) { push(@result,$r); push(@matchingkey,$mk) }
- elsif (@$r) { push(@result,@$r); push(@matchingkey,@$mk) }
- } elsif ($t->isa('Amavis::Lookup::LDAP')) {
- my($r,$mk) = $t->lookup_ldap($addr,$get_all);
- if (!defined $r) {}
- elsif (!$get_all) { push(@result,$r); push(@matchingkey,$mk) }
- elsif (@$r) { push(@result,@$r); push(@matchingkey,@$mk) }
- } elsif ($t->isa('Amavis::Lookup::LDAPattr')) {
- my($r,$mk) = $t->lookup_ldap_attr($addr,$get_all);
- if (!defined $r) {}
- elsif (!$get_all) { push(@result,$r); push(@matchingkey,$mk) }
- elsif (@$r) { push(@result,@$r); push(@matchingkey,@$mk) }
- } else {
- die "TROUBLE: lookup table is an unknown object: " . ref($t);
- }
- last if @result && !$get_all;
- }
- # pretty logging
- if (ll(4)) { # only bother preparing log report which will be printed
- if (defined $label && $label ne '') { $label = " ($label)" }
- if (!@tables) {
- do_log(4,sprintf("lookup%s => undef, %s, no lookup tables",
- $label, fmt_struct($addr)));
- } elsif (!@result) {
- do_log(4,sprintf("lookup%s => undef, %s does not match",
- $label, fmt_struct($addr)));
- } elsif (!$get_all) { # first match wins
- do_log(4,sprintf(
- 'lookup%s => %-6s %s matches, result=%s, matching_key="%s"',
- $label, $result[0] ? 'true,' : 'false,',
- fmt_struct($addr), fmt_struct($result[0]), $matchingkey[0]));
- } else { # want all matches
- do_log(4,sprintf('lookup%s, %d matches for %s, results: %s',
- $label, scalar(@result), fmt_struct($addr),
- join(', ',map { sprintf('"%s"=>%s',
- $matchingkey[$_], fmt_struct($result[$_]))
- } (0..$#result) )));
- }
- }
- if (!$get_all) { !wantarray ? $result[0] : ($result[0], $matchingkey[0]) }
- else { !wantarray ? \@result : (\@result, \@matchingkey) }
- }
- 1;
- #
- package Amavis::Expand;
- use strict;
- use re 'taint';
- BEGIN {
- use Exporter ();
- use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.043';
- @ISA = qw(Exporter);
- @EXPORT_OK = qw(&expand);
- }
- use subs @EXPORT_OK;
- BEGIN {
- import Amavis::Util qw(ll do_log);
- }
- # Given a string reference and a hashref of predefined (builtin) macros,
- # expand() performs a macro expansion and returns a ref to the resulting string
- #
- # This is a simple, yet fully fledged macro processor with proper lexical
- # analysis, call stack, implied quoting levels, user supplied builtin macros,
- # two builtin flow-control macros: selector and iterator, plus a macro #,
- # which discards input tokens until NEWLINE (like 'dnl' in m4).
- # Also recognized are the usual \c and \nnn forms for specifying special
- # characters, where c can be any of: r, n, f, b, e, a, t. Lexical analysis
- # of the input string is performed only once, macro result values are not
- # in danger of being lexically re-parsed and are treated as plain characters,
- # loosing any special meaning they might have. No new macros can be defined
- # by processing input string (at least in this version).
- #
- # Simple caller-provided macros have a single character name (usually a letter)
- # and can evaluate to a string (possibly empty or undef), or an array of
- # strings. It can also be a subroutine reference, in which case the subroutine
- # will be called whenever macro value is needed. The subroutine must return
- # a scalar: a string, or an array reference. The result will be treated as if
- # it were specified directly.
- #
- # Two forms of simple macro calls are known: %x and %#x (where x is a single
- # letter macro name, i.e. a key in a user-supplied associative array):
- # %x evaluates to the hash value associated with the name x;
- # if the value is an array ref, the result is a single concatenated
- # string of values separated with comma-space pairs;
- # %#x evaluates to a number: if a macro value is a scalar, returns 0
- # for all-whitespace value, and 1 otherwise. If a value is an array ref,
- # evaluates to the number of elements in the array.
- # A macro is evaluated only in nonquoted context, i.e. top-level text or in
- # the first argument of a top-level selector (see below). A literal percent
- # character can be produced by %% or \%.
- #
- # More powerful expansion is provided by two builtin macros, using syntax:
- # [? arg1 | arg2 | ... ] a selector
- # [ arg1 | arg2 | ... ] an iterator
- # where [, [?, | and ] are required tokens. To take away the special meaning
- # of these characters they can be quoted by a backslash, e.g. \[ or \\ .
- # Arguments are arbitrary text, possibly multiline, whitespace counts.
- # Nested macro calls are permitted, proper bracket nesting must be observed.
- #
- # SELECTOR lets its first argument be evaluated immediately, and implicitly
- # protects the remaining arguments. The evaluated first argument chooses which
- # of the remaining arguments is selected as a result value. The chosen result
- # is only then evaluated, remaining arguments are discarded without evaluation.
- # The first argument is usually a number (with optional leading and trailing
- # whitespace). If it is a non-numeric string, it is treated as 0 for
- # all-whitespace, and as 1 otherwise. Value 0 selects the very next (second)
- # argument, value 1 selects the one after it, etc. If the value is greater than
- # the number of available arguments, the last one (unless it is the only one)
- # is selected. If there is only one (the first) alternative available but the
- # value is greater than 0, an empty string is returned.
- # Examples:
- # [? 2 | zero | one | two | three ] -> two
- # [? foo | none | any | two | three ] -> any
- # [? 24 | 0 | one | many ] -> many
- # [? 2 |No recipients] -> (empty string)
- # [? %#R |No recipients|One recipient|%#R recipients]
- # [? %q |No quarantine|Quarantined as %q]
- # Note that a selector macro call can be considered a form of if-then-else,
- # except that the 'then' and 'else' parts are swapped!
- #
- # ITERATOR in its full form takes three arguments (and ignores any extra
- # arguments after that):
- # [ %x | body-usually-containing-%x | separator ]
- # All iterator's arguments are implicitly quoted, iterator performs its own
- # substitutions on provided arguments, as described below. The result of an
- # iterator call is a body (the second argument) repeated as many times as
- # there are elements in the array denoted by the first argument. In each
- # instance of a body all occurrences of a token %x in the body are replaced
- # with each consecutive element of the array. Resulting body instances are
- # then glued together with a string given as the third argument. The result
- # is finally evaluated as any top-level text for possible further expansion.
- #
- # There are two simplified forms of iterator call:
- # [ body | separator ]
- # or [ body ]
- # where missing separator is considered a null string, and a missing formal
- # argument name is obtained by looking for the first token of the form %x
- # in the body. If there is no formal argument specified (neither explicitly
- # nor in the body), the result is an empty string, which is potentially useful
- # as a null lexical separator.
- #
- # Examples:
- # [%V| ] a space-separated list of virus names
- #
- # [%V|\n] a newline-separated list of virus names
- #
- # [%V|
- # ] same thing: a newline-separated list of virus names
- #
- # [
- # %V] a list of virus names, each preceeded by NL and spaces
- #
- # [ %R |%s --> <%R>|, ] a comma-space separated list of sender/recipient
- # name pairs where recipient is iterated over the list
- # of recipients. (Only the (first) token %x in the first
- # argument is significant, other characters are ignored.)
- #
- # [%V|[%R|%R + %V|, ]|; ] produce all combinations of %R + %V elements
- #
- # A combined example:
- # [? %#C |#|Cc: [<%C>|, ]]
- # [? %#C ||Cc: [<%C>|, ]\n]# ... same thing
- # evaluates to an empty string if there are no elements in the %C array,
- # otherwise it evaluates to a line: Cc: <addr1>, <addr2>, ...\n
- # The '#' removes input characters until and including newline after it.
- # It can be used for clarity to allow newlines be placed in the source text
- # but not resulting in empty lines in the expanded text. In the second example
- # above, a backslash at the end of the line would achieve the same result,
- # although the method is different: \NEWLINE is removed during initial lexical
- # analysis, while # is an internal macro which, when called, actively discards
- # tokens following it, until NEWLINE (or end of input) is encountered.
- # Whitespace (including newlines) around the first argument %#C of selector
- # call is ignored and can be used for clarity.
- #
- # These all produce the same result:
- # To: [%T|%T|, ]
- # To: [%T|, ]
- # To: %T
- #
- # See further practical examples in the supplied notification messages;
- # see also README.customize file.
- #
- # Author: Mark Martinec <Mark.Martinec@ijs.si>, 2002
- #
- sub expand($$) {
- my($str_ref) = shift; # a ref to a source string to be macro expanded;
- my($builtins_href) = shift; # a hashref, mapping builtin macro names (single
- # char) to macro values: strings or array refs
- my($lex_lbr, $lex_lbrq, $lex_rbr, $lex_sep, $lex_h) =
- \('[', '[?', ']', '|', '#'); # lexical elements to be used as references
- my(%lexmap); # maps string to reference in order to protect lexels
- for (keys(%$builtins_href))
- { $lexmap{"%$_"} = \"%$_"; $lexmap{"%#$_"} = \"%#$_" }
- for ($lex_lbr, $lex_lbrq, $lex_rbr, $lex_sep, $lex_h) { $lexmap{$$_} = $_ }
- # parse lexically
- my(@tokens) = $$str_ref =~ /\G \# | \[\?? | [\]|] | % \#? . | \\ [^0-7] |
- \\ [0-7]{1,3} | [^\[\]\\|%\n#]+ | [^\n]+? | \n /gcsx;
- # replace lexical element strings with object references,
- # unquote backslash-quoted characters and %%, and drop backslash-newlines
- my(%esc) = (r => "\r", n => "\n", f => "\f", b => "\b",
- e => "\e", a => "\a", t => "\t");
- for (@tokens) {
- if (exists $lexmap{$_}) { $_ = $lexmap{$_} } # replace with refs
- elsif ($_ eq "\\\n") { $_ = '' } # drop \NEWLINE
- elsif (/^%(%)\z/) { $_ = $1 } # %% -> %
- elsif (/^(%#?.)\z/s) { $_ = \$1 } # unknown builtins
- elsif (/^\\([0-7]{1,3})\z/) { $_ = chr(oct($1)) } # \nnn
- elsif (/^\\(.)\z/s) { $_ = (exists($esc{$1}) ? $esc{$1} : $1) }
- }
- my($call_level) = 0; my($quote_level) = 0; my(@macro_type, @arg);
- my(%builtins_cached); my($output_str) = ''; my($whereto) = \$output_str;
- while (@tokens) {
- my($t) = shift(@tokens);
- if ($t eq '') { # ignore leftovers
- } elsif ($quote_level>0 && ref($t) && ($t == $lex_lbr || $t == $lex_lbrq)){
- $quote_level++;
- ref($whereto) eq 'ARRAY' ? push(@$whereto, $t) : ($$whereto .= $t);
- } elsif (ref($t) && $t == $lex_lbr) { # begin iterator macro call
- $quote_level++; $call_level++;
- unshift(@arg, [[]]); unshift(@macro_type, ''); $whereto = $arg[0][0];
- } elsif (ref($t) && $t == $lex_lbrq) { # begin selector macro call
- $call_level++; unshift(@arg, [[]]); unshift(@macro_type, '');
- $whereto = $arg[0][0]; $macro_type[0] = 'select';
- } elsif ($quote_level > 1 && ref($t) && $t == $lex_rbr) {
- $quote_level--;
- ref($whereto) eq 'ARRAY' ? push(@$whereto, $t) : ($$whereto .= $t);
- } elsif ($call_level > 0 && ref($t) && $t == $lex_sep) { # next argument
- if ($quote_level == 0 && $macro_type[0] eq 'select' && @{$arg[0]} == 1) {
- $quote_level++;
- }
- if ($quote_level == 1) {
- unshift(@{$arg[0]}, []); $whereto = $arg[0][0]; # begin next arg
- } else {
- ref($whereto) eq 'ARRAY' ? push(@$whereto, $t) : ($$whereto .= $t);
- }
- } elsif ($quote_level > 0 && ref($t) && $t == $lex_rbr) {
- $quote_level--; # quote level just dropped to 0, this is now a call
- $call_level-- if $call_level > 0;
- my(@result);
- if ($macro_type[0] eq 'select') {
- my($sel, @alternatives) = reverse @{$arg[0]}; # list of refs
- # turn reference into a string, avoid warnings about uninitialized val.
- $sel = !ref($sel) ? '' : join('', map {defined $_ ? $_ : ''} @$sel);
- if ($sel =~ /^\s*\z/) { $sel = 0 }
- elsif ($sel =~ /^\s*(\d+)\s*\z/) { $sel = 0+$1 } # make numeric
- else { $sel = 1 }
- # provide an empty second alternative if we only have one specified
- push(@alternatives, []) if @alternatives < 2 && $sel > 0;
- if ($sel < 0) { $sel = 0 }
- elsif ($sel > $#alternatives) { $sel = $#alternatives }
- @result = @{$alternatives[$sel]};
- } else { # iterator
- my($cvar_r, $sep_r, $body_r, $cvar); # give meaning to arguments
- if (@{$arg[0]} >= 3) { ($cvar_r,$body_r,$sep_r) = reverse @{$arg[0]} }
- else { ($body_r, $sep_r) = reverse @{$arg[0]}; $cvar_r = $body_r }
- # find the formal argument name (iterator)
- for (@$cvar_r) {
- if (ref && $$_ =~ /^%(.)\z/s) { $cvar = $1; last }
- }
- if (exists($builtins_href->{$cvar})) {
- my($values_r);
- if (exists($builtins_cached{$cvar})) {
- $values_r = $builtins_cached{$cvar};
- } else {
- $values_r = $builtins_href->{$cvar};
- while (ref($values_r) eq 'CODE') { $values_r = &$values_r }
- $builtins_cached{$cvar} = $values_r;
- }
- $values_r = [$values_r] if !ref($values_r);
- my($ind);
- my($re) = qr/^%\Q$cvar\E\z/;
- for my $val (@$values_r) {
- push(@result, @$sep_r) if ++$ind > 1 && ref($sep_r);
- push(@result, map { (ref && $$_ =~ /$re/) ? $val : $_ } @$body_r);
- }
- }
- }
- shift(@macro_type); # pop the call stack
- shift(@arg);
- $whereto = $call_level > 0 ? $arg[0][0] : \$output_str;
- unshift(@tokens, @result); # active macro call, evaluate result
- } else { # quoted, plain string, simple macro call, or a misplaced token
- my($s) = '';
- if ($quote_level > 0 || !ref($t)) {
- $s = $t; # quoted or string
- } elsif ($t == $lex_h) { # discard tokens to (and including) newline
- while (@tokens) { last if shift(@tokens) eq "\n" }
- } elsif ($$t =~ /^%(\#)?(.)\z/s) { # macro call %#x or %x
- my($num,$m) = ($1,$2);
- if (!exists($builtins_href->{$m})) { $s = '' } # no such
- elsif (exists($builtins_cached{$m})) { $s = $builtins_cached{$m} }
- else {
- $s = $builtins_href->{$m};
- while (ref($s) eq 'CODE') { $s = &$s } # subroutine callback
- $builtins_cached{$m} = $s;
- }
- if (defined $num && $num eq '#') { # macro call form %#x
- # for array: number of elements; for scalar: nonwhite=1, other 0
- $s = ref($s) ? @$s : $s !~ /^\s*\z/ ? 1 : 0;
- } else { # macro call %x evaluates to the value of macro x
- $s = join(', ', @$s) if ref $s;
- }
- } else { $s = $$t } # misplaced token, e.g. a top level | or ]
- ref($whereto) eq 'ARRAY' ? push(@$whereto, $s) : ($$whereto .= $s);
- }
- }
- \$output_str;
- }
- 1;
- #
- package Amavis::IO::Zlib;
- # A simple IO::File -compatible wrapper around Compress::Zlib,
- # much like IO::Zlib but simpler: does only what we need and does it carefully
- use strict;
- use re 'taint';
- BEGIN {
- use Exporter ();
- use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.043';
- @ISA = qw(Exporter);
- }
- use Errno qw(EIO);
- use Compress::Zlib;
- sub new {
- my($class) = shift; my($self) = bless {}, $class;
- if (@_) { $self->open(@_) or return undef }
- $self;
- }
- sub close {
- my($self) = shift;
- my($status); eval { $status = $self->{fh}->gzclose }; delete $self->{fh};
- if ($status != Z_OK || $@ ne '') {
- die "gzclose error: $gzerrno"; # can't stash arbitrary text into $!
- $! = EIO; return undef; # not reached
- }
- 1;
- }
- sub DESTROY {
- my($self) = shift;
- if (ref $self && $self->{fh}) { eval { $self->close } }
- }
- sub open {
- my($self,$fname,$mode) = @_;
- delete $self->{fh};
- $self->{fname} = $fname; $self->{mode} = $mode; $self->{pos} = 0;
- my($gz) = gzopen($fname,$mode);
- if ($gz) { $self->{fh} = $gz }
- else {
- die "gzopen error: $gzerrno"; # can't stash arbitrary text into $!
- $! = EIO; undef $gz; # not reached
- }
- $gz;
- }
- sub seek {
- my($self,$pos,$whence) = @_;
- $whence==0 && $pos==0
- or die "Seek to $whence,$pos on gzipped file not supported";
- $self->{mode} eq 'rb'
- or die "Seek to $whence,$pos on gzipped file only supported for 'rb' mode";
- if ($self->{pos}==0) { 1 } # already there
- else { $self->close; $self->open($self->{fname},$self->{mode}) }
- }
- sub read { # SCALAR,LENGTH,OFFSET
- my($self) = shift; $self->{pos} = 1;
- !defined($_[2]) || $_[2]==0
- or die "Reading gzipped file to an offset not supported";
- my($nbytes) = $self->{fh}->gzread($_[0], defined $_[1] ? $_[1] : 4096);
- if ($nbytes < 0) {
- die "gzread error: $gzerrno"; # can't stash arbitrary text into $!
- $! = EIO; undef $nbytes; # not reached
- }
- $nbytes; # eof: 0; error: undef
- }
- sub getline {
- my($self) = shift; $self->{pos} = 1; my($nbytes,$line);
- $nbytes = $self->{fh}->gzreadline($line);
- if ($nbytes <= 0) { # eof (0) or error (-1)
- $! = 0; undef $line;
- if ($nbytes < 0 && $gzerrno != Z_STREAM_END) {
- die "gzreadline error: $gzerrno"; # can't stash arbitrary text into $!
- $! = EIO; # not reached
- }
- }
- $line; # eof: undef, $! zero; error: undef, $! nonzero
- }
- sub print {
- my($self) = shift;
- my($nbytes); my($len) = length($_[0]);
- if ($len <= 0) { $nbytes = "0 but true" }
- else {
- $self->{pos} = 1; $nbytes = $self->{fh}->gzwrite($_[0]);
- if ($nbytes <= 0) {
- die "gzwrite error: $gzerrno"; # can't stash arbitrary text into $!
- $! = EIO; undef $nbytes; # not reached
- }
- }
- $nbytes;
- }
- sub printf { shift->print(sprintf(shift,@_)) }
- 1;
- #
- package Amavis::In::Connection;
- # Keeps relevant information about how we received the message:
- # client connection information, SMTP envelope and SMTP parameters
- use strict;
- use re 'taint';
- BEGIN {
- use Exporter ();
- use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.043';
- @ISA = qw(Exporter);
- }
- sub new
- { my($class) = @_; bless {}, $class }
- sub client_ip # client IP address (immediate SMTP client, i.e. our MTA)
- { my($self)=shift; !@_ ? $self->{client_ip} : ($self->{client_ip}=shift) }
- sub socket_ip # IP address of our interface that received connection
- { my($self)=shift; !@_ ? $self->{socket_ip} : ($self->{socket_ip}=shift) }
- sub socket_port # TCP port of our interface that received connection
- { my($self)=shift; !@_ ? $self->{socket_port}:($self->{socket_port}=shift) }
- sub proto # TCP/UNIX
- { my($self)=shift; !@_ ? $self->{proto} : ($self->{proto}=shift) }
- sub smtp_proto # SMTP/ESMTP(A|S|SA)/LMTP(A|S|SA) # rfc3848, or QMQP/QMQPqq
- { my($self)=shift; !@_ ? $self->{smtp_proto}: ($self->{smtp_proto}=shift) }
- sub smtp_helo # (E)SMTP HELO/EHLO parameter
- { my($self)=shift; !@_ ? $self->{smtp_helo} : ($self->{smtp_helo}=shift) }
- 1;
- #
- package Amavis::In::Message::PerRecip;
- use strict;
- use re 'taint';
- BEGIN {
- use Exporter ();
- use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.043';
- @ISA = qw(Exporter);
- }
- # per-recipient data are kept in an array of n-tuples:
- # (recipient-address, destiny, done, smtp-response-text, remote-mta, ...)
- sub new # NOTE: this class is a list for historical reasons, not a hash
- { my($class) = @_; bless [(undef) x 15], $class }
- # subs to set or access individual elements of a n-tuple by name
- sub recip_addr # raw (unquoted) recipient envelope e-mail address
- { my($self)=shift; !@_ ? $$self[0] : ($$self[0]=shift) }
- sub recip_addr_modified # recip. addr. with possible addr. extension inserted
- { my($self)=shift; !@_ ? $$self[1] : ($$self[1]=shift) }
- sub recip_destiny # D_REJECT, D_BOUNCE, D_DISCARD, D_PASS
- { my($self)=shift; !@_ ? $$self[2] : ($$self[2]=shift) }
- sub recip_done # false: not done, true: done (1: faked, 2: truly sent)
- { my($self)=shift; !@_ ? $$self[3] : ($$self[3]=shift) }
- sub recip_smtp_response # rfc2821 response (3-digit + enhanced resp + text)
- { my($self)=shift; !@_ ? $$self[4] : ($$self[4]=shift) }
- sub recip_remote_mta_smtp_response # smtp response as issued by remote MTA
- { my($self)=shift; !@_ ? $$self[5] : ($$self[5]=shift) }
- sub recip_remote_mta # remote MTA that issued the smtp response
- { my($self)=shift; !@_ ? $$self[6] : ($$self[6]=shift) }
- sub recip_mbxname # mailbox name or file when known (local:, bsmtp: or sql:)
- { my($self)=shift; !@_ ? $$self[7] : ($$self[7]=shift) }
- sub recip_whitelisted_sender # recip considers this sender whitelisted (> 0)
- { my($self)=shift; !@_ ? $$self[8] : ($$self[8]=shift) }
- sub recip_blacklisted_sender # recip considers this sender blacklisted
- { my($self)=shift; !@_ ? $$self[9] : ($$self[9]=shift) }
- sub recip_score_boost # recip adds penalty spam points to the final score
- { my($self)=shift; !@_ ? $$self[10] : ($$self[10]=shift) }
- sub infected # contains a virus (1); check bypassed (undef); clean (0)
- { my($self)=shift; !@_ ? $$self[11] : ($$self[11]=shift) }
- sub banned_parts # banned part descriptions (ref to a list of banned parts)
- { my($self)=shift; !@_ ? $$self[12] : ($$self[12]=shift) }
- sub banned_keys # keys of matching banned rules (a ref to a list)
- { my($self)=shift; !@_ ? $$self[13] : ($$self[13]=shift) }
- sub banned_rhs # right-hand side of matching rules (a ref to a list)
- { my($self)=shift; !@_ ? $$self[14] : ($$self[14]=shift) }
- sub recip_final_addr { # return recip_addr_modified if set, else recip_addr
- my($self)=shift;
- my($newaddr) = $self->recip_addr_modified;
- defined $newaddr ? $newaddr : $self->recip_addr;
- }
- 1;
- #
- package Amavis::In::Message;
- # this class contains information about the message being processed
- use strict;
- use re 'taint';
- BEGIN {
- use Exporter ();
- use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.043';
- @ISA = qw(Exporter);
- }
- BEGIN {
- import Amavis::Conf qw(:platform);
- import Amavis::rfc2821_2822_Tools qw(rfc2822_timestamp);
- import Amavis::In::Message::PerRecip;
- }
- sub new
- { my($class) = @_; bless {}, $class }
- sub rx_time # Unix time (s since epoch) of message reception by amavisd
- { my($self)=shift; !@_ ? $self->{rx_time} : ($self->{rx_time}=shift) }
- sub client_addr # original client IP addr, obtained from XFORWARD or milter
- { my($self)=shift; !@_ ? $self->{cli_ip} : ($self->{cli_ip}=shift) }
- sub client_name # orig. client DNS name, obtained from XFORWARD or milter
- { my($self)=shift; !@_ ? $self->{cli_name} : ($self->{cli_name}=shift) }
- sub client_proto # orig. client protocol, obtained from XFORWARD or milter
- { my($self)=shift; !@_ ? $self->{cli_proto} : ($self->{cli_proto}=shift) }
- sub client_helo # orig. client EHLO name, obtained from XFORWARD or milter
- { my($self)=shift; !@_ ? $self->{cli_helo} : ($self->{cli_helo}=shift) }
- sub queue_id # MTA queue ID of message if known (Courier, milter/AM.PDP)
- { my($self)=shift; !@_ ? $self->{queue_id} : ($self->{queue_id}=shift) }
- sub mail_id # some long-term unique id of the message on this system
- { my($self)=shift; !@_ ? $self->{mail_id} : ($self->{mail_id}=shift) }
- sub secret_id # secret string to grant access to message with mail_id
- { my($self)=shift; !@_ ? $self->{secret_id} : ($self->{secret_id}=shift) }
- sub msg_size # ESMTP SIZE value, later corrected by actual message size
- { my($self)=shift; !@_ ? $self->{msg_size} : ($self->{msg_size}=shift) }
- sub auth_user # ESMTP AUTH username
- { my($self)=shift; !@_ ? $self->{auth_user} : ($self->{auth_user}=shift) }
- sub auth_pass # ESMTP AUTH password
- { my($self)=shift; !@_ ? $self->{auth_pass} : ($self->{auth_pass}=shift) }
- sub auth_submitter # ESMTP MAIL command AUTH option value (addr-spec or "<>")
- { my($self)=shift; !@_ ? $self->{auth_subm} : ($self->{auth_subm}=shift) }
- sub requested_by # Resent-From addr who requested release from a quarantine
- { my($self)=shift; !@_ ? $self->{requested_by}:($self->{requested_by}=shift)}
- sub body_type # ESMTP BODY param (rfc1652: 7BIT, 8BITMIME) or BINARYMIME
- { my($self)=shift; !@_ ? $self->{body_type} : ($self->{body_type}=shift) }
- sub sender # envelope sender
- { my($self)=shift; !@_ ? $self->{sender} : ($self->{sender}=shift) }
- sub sender_contact # unmangled sender address or undef (e.g. believed faked)
- { my($self)=shift; !@_ ? $self->{sender_c} : ($self->{sender_c}=shift) }
- sub sender_source # unmangled sender address or info from the trace
- { my($self)=shift; !@_ ? $self->{sender_src} : ($self->{sender_src}=shift) }
- sub mime_entity # MIME::Parser entity holding the message
- { my($self)=shift; !@_ ? $self->{mime_entity}: ($self->{mime_entity}=shift)}
- sub parts_root # Amavis::Unpackers::Part root object
- { my($self)=shift; !@_ ? $self->{parts_root}: ($self->{parts_root}=shift)}
- sub mail_text # rfc2822 msg: (open) file handle, or MIME::Entity object
- { my($self)=shift; !@_ ? $self->{mail_text} : ($self->{mail_text}=shift) }
- sub mail_text_fn # orig. mail filename or undef, e.g. mail_tempdir/email.txt
- { my($self)=shift; !@_ ? $self->{mail_text_fn} : ($self->{mail_text_fn}=shift) }
- sub mail_tempdir # work directory, under $TEMPBASE or supplied by client
- { my($self)=shift; !@_ ? $self->{mail_tempdir} : ($self->{mail_tempdir}=shift) }
- sub header_edits # Amavis::Out::EditHeader object or undef
- { my($self)=shift; !@_ ? $self->{hdr_edits} : ($self->{hdr_edits}=shift) }
- sub orig_header # original header - an arrayref of lines, with trailing LF
- { my($self)=shift; !@_ ? $self->{orig_header}: ($self->{orig_header}=shift) }
- sub orig_header_size # size of original header
- { my($self)=shift; !@_ ? $self->{orig_hdr_s} : ($self->{orig_hdr_s}=shift) }
- sub orig_body_size # size of original body
- { my($self)=shift; !@_ ? $self->{orig_bdy_s} : ($self->{orig_bdy_s}=shift) }
- sub body_digest # message digest of a message body (e.g. MD5 or SHA1)
- { my($self)=shift; !@_ ? $self->{body_digest}: ($self->{body_digest}=shift) }
- sub quarantined_to # list of quarantine mailbox names or addresses if quarantined
- { my($self)=shift; !@_ ? $self->{quarantine} : ($self->{quarantine}=shift) }
- sub quar_type # quarantine type: F/Z/B/Q/M (file/zipfile/bsmtp/sql/mailbox)
- { my($self)=shift; !@_ ? $self->{quar_type} : ($self->{quar_type}=shift) }
- sub dsn_sent # delivery status notification was sent(1) or faked(2)
- { my($self)=shift; !@_ ? $self->{dsn_sent} : ($self->{dsn_sent}=shift) }
- sub delivery_method # delivery method, or empty for implicit delivery (milter)
- { my($self)=shift; !@_ ? $self->{delivery_method}:($self->{delivery_method}=shift)}
- sub client_delete # don't delete the tempdir, it is a client's reponsibility
- { my($self)=shift; !@_ ? $self->{client_delete}:($self->{client_delete}=shift)}
- # credativ -jw
- sub postfixid # the original postfix queue id
- { my($self)=shift; !@_ ? $self->{postfixid} : ($self->{postfixid}=shift) }
- # credativ end
- # The order of entries in the list is the original order in which
- # recipient addresses (e.g. obtained via 'MAIL TO:') were received.
- # Only the entries that were accepted (via SMTP response code 2xx)
- # are placed in the list. The ORDER MUST BE PRESERVED and no recipients
- # may be added or removed from the list! This is vital to be able
- # to produce correct per-recipient responses to a LMTP client!
- #
- sub per_recip_data { # get or set a listref of envelope recipient n-tuples
- my($self) = shift;
- # store a given listref of n-tuples (originals, not copies!)
- if (@_) { @{$self->{recips}} = @{$_[0]} }
- # return a listref to the original n-tuples,
- # caller may modify the data if he knows what he is doing
- $self->{recips};
- }
- sub recips { # get or set a listref of envelope recipients
- my($self)=shift;
- if (@_) { # store a copy of a given listref of recipient addresses
- # wrap scalars (strings) into n-tuples
- $self->per_recip_data([ map {
- my($per_recip_obj) = Amavis::In::Message::PerRecip->new;
- $per_recip_obj->recip_addr($_);
- $per_recip_obj->recip_destiny(D_PASS); # default is Pass
- $per_recip_obj } @{$_[0]} ]);
- }
- return if !defined wantarray; # don't bother
- # return listref of recipient addresses
- [ map { $_->recip_addr } @{$self->per_recip_data} ];
- }
- 1;
- #
- package Amavis::Out::EditHeader;
- # Accumulates instructions on what lines need to be added to the message
- # header, deleted, or how to change existing lines, then via a call
- # to write_header() performs these edits on the fly.
- use strict;
- use re 'taint';
- BEGIN {
- use Exporter ();
- use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.043';
- @ISA = qw(Exporter);
- @EXPORT_OK = qw(&hdr);
- }
- BEGIN {
- import Amavis::Conf qw(:platform c cr ca);
- import Amavis::Timing qw(section_time);
- import Amavis::Util qw(ll do_log safe_encode q_encode);
- }
- use MIME::Words;
- sub new { my($class) = @_; bless {}, $class }
- sub prepend_header($$$;$) {
- my($self, $field_name, $field_body, $structured) = @_;
- unshift(@{$self->{prepend}}, hdr($field_name, $field_body, $structured));
- }
- sub append_header($$$;$) {
- my($self, $field_name, $field_body, $structured) = @_;
- push(@{$self->{append}}, hdr($field_name, $field_body, $structured));
- }
- sub delete_header($$) {
- my($self, $field_name) = @_;
- $self->{edit}{lc($field_name)} = undef;
- }
- sub edit_header($$$;$) {
- my($self, $field_name, $field_edit_sub, $structured) = @_;
- # $field_edit_sub will be called with 2 args: field name and field body;
- # it should return the replacement field body (no field name and colon),
- # with or without the trailing NL
- !defined($field_edit_sub) || ref($field_edit_sub) eq 'CODE'
- or die "edit_header: arg#3 must be undef or a subroutine ref";
- $self->{edit}{lc($field_name)} = $field_edit_sub;
- }
- # copy all header edits from another header-edits object into this one
- sub inherit_header_edits($$) {
- my($self, $other_edits) = @_;
- if (defined $other_edits) {
- unshift(@{$self->{prepend}},
- @{$other_edits->{prepend}}) if $other_edits->{prepend};
- unshift(@{$self->{append}},
- @{$other_edits->{append}}) if $other_edits->{append};
- if ($other_edits->{edit}) {
- for (keys %{$other_edits->{edit}})
- { $self->{edit}{$_} = $other_edits->{edit}{$_} }
- }
- }
- }
- # Insert space after colon if not present, RFC2047-encode if field body
- # contains non-ASCII characters, fold long lines if needed,
- # prepend space before each NL if missing, append NL if missing;
- # Header fields with only spaces are not allowed.
- # (rfc2822: Each line of characters MUST be no more than 998 characters,
- # and SHOULD be no more than 78 characters, excluding the CRLF.
- # '$structured' indicates that folding is only allowed at positions
- # indicated by \n in the provided header body.
- #
- sub hdr($$;$) {
- my($field_name, $field_body, $structured) = @_;
- if ($field_name =~ /^(X-.*|Subject|Comments)\z/si &&
- $field_body =~ /[^\011\012\040-\176]/ #any nonprintable except TAB and LF
- ) { # encode according to RFC 2047
- $field_body =~ s/\n([ \t])/$1/g; # unfold
- chomp($field_body);
- my($field_body_octets) = safe_encode(c('hdr_encoding'), $field_body);
- my($qb) = c('hdr_encoding_qb');
- if (uc($qb) eq 'Q') {
- $field_body = q_encode($field_body_octets, $qb, c('hdr_encoding'));
- } else {
- $field_body = MIME::Words::encode_mimeword($field_body_octets,
- $qb, c('hdr_encoding'));
- }
- } else { # supposed to be in plain ASCII, let's make sure it is
- $field_body = safe_encode('ascii', $field_body);
- }
- $field_name = safe_encode('ascii', $field_name);
- my($str) = $field_name . ':';
- $str .= ' ' if $field_body !~ /^[ \t]/;
- $str .= $field_body;
- $str =~ s/\n([^ \t\n])/\n $1/g; # insert a space at line folds if missing
- $str =~ s/\n([ \t]*\n)+/\n/g; # remove empty lines
- chomp($str); # chop off trailing NL if present
- if ($structured) {
- $str =~ s/[ \t]+/ /g; # collapse spaces and tabs to a single space
- my(@sublines) = split(/\n/, $str, -1);
- $str = ''; my($s) = ''; my($s_l) = 0; my($s_il)=0;
- for (@sublines) { # join shorter field sections
- if ($s !~ /^\s*\z/ && $s_l + $s_il + length($_) > 78) {
- $s_il = 8; # length of the initial tab
- $str .= "\n\t" if $str ne '';
- $s =~ s/^[ \t]+//g; # remove leading and trailing whitespace
- $s =~ s/[ \t]+$//g;
- $str .= $s; $s = ''; $s_l = 0;
- }
- $s .= $_; $s_l += length($_);
- }
- if ($s !~ /^\s*\z/) {
- $str .= "\n\t" if $str ne '';
- $s =~ s/^[ \t]+//g; # remove leading and trailing whitespace
- $s =~ s/[ \t]+$//g;
- $str .= $s;
- }
- } elsif (length($str) > 998) {
- # truncate the damn thing (to be done better)
- $str = substr($str,0,998);
- }
- $str .= "\n"; # append final NL
- do_log(5, "header: $str");
- $str;
- }
- # Copy mail header to the supplied method (line by line) while adding,
- # removing, or changing certain header lines as required, and append
- # an empty line (end-of-header). Returns number of original 'Received:'
- # header fields to make simple loop detection possible (as required
- # by rfc2821 section 6.2).
- #
- # Assumes input file is properly positioned, leaves it positioned
- # at the beginning of the body.
- #
- sub write_header($$$) {
- my($self, $msg, $out_fh) = @_;
- my($is_mime) = ref($msg) && $msg->isa('MIME::Entity') ? 1 : 0;
- do_log(5,"write_header: $is_mime, $out_fh");
- $out_fh = IO::Wrap::wraphandle($out_fh); # assure an IO::Handle-like obj
- my(@header);
- if ($is_mime) {
- @header = map { /^[ \t]*\n?\z/ ? () # remove empty lines, ensure NL
- : (/\n\z/ ? $_ : $_ . "\n") } @{$msg->header};
- }
- my($received_cnt) = 0; my($str) = '';
- for (@{$self->{prepend}}) { $str .= $_ }
- if ($str ne '') { $out_fh->print($str) or die "sending mail header1: $!" }
- if (!defined($msg)) {
- # existing header empty
- } else {
- push(@header, $eol) if $is_mime; # append empty line as end-of-header
- local($1,$2); my($curr_head,$next_head); my($illcnt) = 0; undef $!;
- while (defined($next_head = $is_mime ? shift @header : $msg->getline)) {
- if ($next_head =~ /^[ \t]/) { $curr_head .= $next_head } # folded
- else { # new header
- if (!defined($curr_head)) { # no previous complete header field
- } elsif ($curr_head !~ /^([!-9;-\176]+)[ \t]*:(.*)\z/s) {
- # invalid header, but we don't care
- $curr_head =~ s{\n [ \t]* (?= \n )}{}gsx and $illcnt++;
- $out_fh->print($curr_head) or die "sending mail header4: $!";
- } else { # count, edit, or delete
- # obsolete rfc822 syntax allowed whitespace before colon
- my($field_name, $field_body) = ($1, $2);
- my($field_name_lc) = lc($field_name);
- $received_cnt++ if $field_name_lc eq 'received';
- if (!exists($self->{edit}{$field_name_lc})) { # unchanged
- # unfold illegal all-whitespace continuation lines
- $curr_head =~ s{\n [ \t]* (?= \n )}{}gsx and $illcnt++;
- $out_fh->print($curr_head) or die "sending mail header5: $!";
- } else {
- my($edit) = $self->{edit}{$field_name_lc};
- if (defined($edit)) { # edit, not delete
- chomp($field_body);
- ### $field_body =~ s/\n([ \t])/$1/g; # unfold
- my($subst) = hdr($field_name, &$edit($field_name,$field_body));
- $subst =~ s{\n [ \t]* (?= \n )}{}gsx and $illcnt++;
- $out_fh->print($subst) or die "sending mail header6: $!";
- }
- }
- }
- last if $next_head eq $eol; # end-of-header reached
- $curr_head = $next_head;
- }
- undef $!;
- }
- defined $next_head || $is_mime || $!==0
- or die "Error reading mail header: $!";
- do_log(0, "INFO: unfolded $illcnt illegal all-whitespace ".
- "continuation lines") if $illcnt;
- }
- $str = '';
- for (@{$self->{append}}) { $str .= $_ }
- $str .= $eol; # end of header - separator line
- $out_fh->print($str) or die "sending mail header7: $!";
- section_time('write-header');
- $received_cnt;
- }
- 1;
- #
- package Amavis::Out::Local;
- use strict;
- use re 'taint';
- BEGIN {
- use Exporter ();
- use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.043';
- @ISA = qw(Exporter);
- @EXPORT_OK = qw(&mail_to_local_mailbox);
- }
- use Errno qw(ENOENT EACCES);
- use IO::File qw(O_CREAT O_EXCL O_WRONLY);
- use IO::Wrap;
- BEGIN {
- import Amavis::Conf qw(:platform $quarantine_subdir_levels c cr ca);
- import Amavis::Lock;
- import Amavis::Util qw(ll do_log am_id exit_status_str run_command_consumer);
- import Amavis::Timing qw(section_time);
- import Amavis::rfc2821_2822_Tools;
- import Amavis::Out::EditHeader;
- }
- use subs @EXPORT_OK;
- # Deliver to local mailboxes only, ignore the rest: either to directory
- # (maildir style), or file (Unix mbox). (normally used as a quarantine method)
- #
- sub mail_to_local_mailbox(@) {
- my($via, $msginfo, $initial_submission, $filter) = @_;
- $via =~ /^local:(.*)\z/si or die "Bad local method: $via";
- my($via_arg) = $1;
- my(@per_recip_data) = grep { !$_->recip_done && (!$filter || &$filter($_)) }
- @{$msginfo->per_recip_data};
- return 1 if !@per_recip_data;
- my($msg) = $msginfo->mail_text; # a file handle or a MIME::Entity object
- if (defined($msg) && !$msg->isa('MIME::Entity')) {
- # at this point, we have no idea what the user gave us...
- # a globref? a FileHandle?
- $msg = IO::Wrap::wraphandle($msg); # now we have an IO::Handle-like obj
- }
- my($sender) = $msginfo->sender;
- for my $r (@per_recip_data) {
- # each recipient gets its own copy; these are not the original recipients
- my($recip) = $r->recip_final_addr;
- next if $recip eq '';
- my($localpart,$domain) = split_address($recip);
- my($smtp_response);
- # %local_delivery_aliases emulates aliases map - this would otherwise
- # be done by MTA's local delivery agent if we gave the message to MTA.
- # This way we keep interface compatible with other mail delivery
- # methods. The hash value may be a ref to a pair of fixed strings,
- # or a subroutine ref (which must return such pair) to allow delayed
- # (lazy) evaluation when some part of the pair is not yet known
- # at initialization time.
- # If no matching entry is found, the key ($localpart) is treated as
- # a mailbox filename if nonempty, or else quarantining is skipped.
- my($mbxname, $suggested_filename);
- { # a block is used as a 'switch' statement - 'last' will exit from it
- my($ldar) = cr('local_delivery_aliases'); # a ref to a hash
- my($alias) = $ldar->{$localpart};
- if (ref($alias) eq 'ARRAY') {
- ($mbxname, $suggested_filename) = @$alias;
- } elsif (ref($alias) eq 'CODE') { # lazy (delayed) evaluation
- ($mbxname, $suggested_filename) = &$alias;
- } elsif ($alias ne '') {
- ($mbxname, $suggested_filename) = ($alias, undef);
- } elsif (!exists $ldar->{$localpart}) {
- do_log(0, "no key '$localpart' in \%local_delivery_aliases, skip local delivery");
- }
- if ($mbxname eq '') {
- my($why) = !exists $ldar->{$localpart} ? 1 : $alias eq '' ? 2 : 3;
- do_log(2, "skip local delivery($why): <$sender> -> <$recip>");
- $smtp_response = "250 2.6.0 Ok, skip local delivery($why)";
- last; # exit block, not the loop
- }
- my($ux); # is it a UNIX-style mailbox?
- if (!-d $mbxname) { # assume a filename (need not exist yet)
- $ux = 1; # $mbxname is a UNIX-style mailbox (one file)
- } else { # a directory
- $ux = 0; # $mbxname is a directory (amavis/maildir style mailbox)
- my($explicitly_suggested_filename) = $suggested_filename ne '';
- if ($suggested_filename eq '')
- { $suggested_filename = $via_arg ne '' ? $via_arg : '%m' }
- $suggested_filename =~ s{%(.)}
- { $1 eq 'b' ? $msginfo->body_digest
- : $1 eq 'm' ? $msginfo->mail_id
- : $1 eq 'i' ? iso8601_timestamp($msginfo->rx_time,1,'-')
- : $1 eq 'n' ? am_id()
- : $1 eq '%' ? '%' : '%'.$1 }egs;
- $mbxname = "$mbxname/$suggested_filename";
- if ($quarantine_subdir_levels>=1 && !$explicitly_suggested_filename) {
- # using a subdirectory structure to disperse quarantine files
- local($1,$2); my($subdir) = substr($msginfo->mail_id, 0, 1);
- $subdir=~/^[A-Z0-9]\z/i or die "Unexpected first char: $subdir";
- $mbxname =~ m{^ (.*/)? ([^/]+) \z}sx; my($path,$fname) = ($1,$2);
- $mbxname = "$path$subdir/$fname"; # resulting full filename
- my($errn) = stat("$path$subdir") ? 0 : 0+$!;
- if ($errn == ENOENT) { # check/prepare a set of subdirectories
- do_log(2, "checking/creating quarantine subdirs under $path");
- for my $d ('A'..'Z','a'..'z','0'..'9') {
- $errn = stat("$path$d") ? 0 : 0+$!;
- if ($errn == ENOENT) {
- mkdir("$path$d", 0750) or die "Can't create dir $path$d: $!";
- }
- }
- }
- }
- }
- do_log(1, "local delivery: <$sender> -> <$recip>, mbx=$mbxname");
- my($mp,$pos,$pid);
- my($errn) = stat($mbxname) ? 0 : 0+$!;
- local $SIG{CHLD} = 'DEFAULT';
- local $SIG{PIPE} = 'IGNORE'; # write to broken pipe would throw a signal
- eval { # try to open the mailbox file for writing
- if (!$ux) { # one mail per file, will create specified file
- if ($errn == ENOENT) {} # good, no file, as expected
- elsif (!$errn && -f _)
- { die "File $mbxname already exists, refuse to overwrite" }
- else
- { die "File $mbxname exists??? Refuse to overwrite it, $!" }
- if ($mbxname =~ /\.gz\z/) {
- $mp = Amavis::IO::Zlib->new;
- $mp->open($mbxname,'wb')
- or die "Can't create gzip file $mbxname: $!";
- } else {
- $mp = IO::File->new;
- $mp->open($mbxname, O_CREAT|O_EXCL|O_WRONLY, 0640)
- or die "Can't create file $mbxname: $!";
- binmode($mp, ":bytes") or die "Can't cancel :utf8 mode: $!"
- if $unicode_aware;
- }
- } else { # append to UNIX-style mailbox
- # deliver only to non-executable regular files
- if ($errn == ENOENT) {
- $mp = IO::File->new;
- $mp->open($mbxname, O_CREAT|O_EXCL|O_WRONLY, 0640)
- or die "Can't create file $mbxname: $!";
- } elsif (!$errn && !-f _) {
- die "Mailbox $mbxname is not a regular file, refuse to deliver";
- } elsif (-x _ || -X _) {
- die "Mailbox file $mbxname is executable, refuse to deliver";
- } else {
- $mp = IO::File->new;
- $mp->open($mbxname,'>>',0640)
- or die "Can't append to $mbxname: $!";
- }
- binmode($mp, ":bytes") or die "Can't cancel :utf8 mode: $!"
- if $unicode_aware;
- lock($mp);
- $mp->seek(0,2) or die "Can't position mailbox file to its tail: $!";
- $pos = $mp->tell;
- }
- if (defined($msg) && !$msg->isa('MIME::Entity'))
- { $msg->seek(0,0) or die "Can't rewind mail file: $!" }
- };
- if ($@ ne '') {
- chomp($@);
- $smtp_response = $@ eq "timed out" ? "450 4.4.2" : "451 4.5.0";
- $smtp_response .= " Local delivery(1) to $mbxname failed: $@";
- last; # exit block, not the loop
- }
- eval { # if things fail from here on, try to restore mailbox state
- if ($ux) {
- $mp->printf("From %s %s$eol", quote_rfc2821_local($sender),
- scalar(localtime($msginfo->rx_time)) ) # English date!
- or die "Can't write to $mbxname: $!";
- }
- my($hdr_edits) = $msginfo->header_edits;
- if (!$hdr_edits) {
- $hdr_edits = Amavis::Out::EditHeader->new;
- $msginfo->header_edits($hdr_edits);
- }
- $hdr_edits->delete_header('Return-Path');
- $hdr_edits->prepend_header('Delivered-To',
- quote_rfc2821_local($recip));
- $hdr_edits->prepend_header('Return-Path',
- qquote_rfc2821_local($sender));
- my($received_cnt) = $hdr_edits->write_header($msg,$mp);
- if ($received_cnt > 110) {
- # loop detection required by rfc2821 section 6.2
- # Do not modify the signal text, it gets matched elsewhere!
- die "Too many hops: $received_cnt 'Received:' header lines\n";
- }
- if (!$ux) { # do it in blocks for speed if we can
- my($nbytes,$buff);
- while (($nbytes=$msg->read($buff,16384)) > 0)
- { $mp->print($buff) or die "Can't write to $mbxname: $!" }
- defined $nbytes or die "Error reading: $!";
- } else { # for UNIX-style mailbox delivery: escape 'From '
- my($ln); my($blank_line) = 1;
- for (undef $!; defined($ln=$msg->getline); undef $!) {
- $mp->print('>') or die "Can't write to $mbxname: $!"
- if $blank_line && $ln=~/^From /;
- $mp->print($ln) or die "Can't write to $mbxname: $!";
- $blank_line = $ln eq $eol;
- }
- defined $ln || $!==0 or die "Error reading: $!";
- }
- # must append an empty line for a Unix mailbox format
- $mp->print($eol) or die "Can't write to $mbxname: $!" if $ux;
- };
- my($failed) = 0;
- if ($@ ne '') { # trouble
- chomp($@);
- if ($ux && defined($pos) && $can_truncate) {
- # try to restore UNIX-style mailbox to previous size;
- # Produces a fatal error if truncate isn't implemented on the system
- $mp->truncate($pos) or die "Can't truncate file $mbxname: $!";
- }
- $failed = 1;
- }
- unlock($mp) if $ux;
- $mp->close or die "Error closing $mbxname: $!";
- if (!$failed) {
- $smtp_response = "250 2.6.0 Ok, delivered to $mbxname";
- } elsif ($@ eq "timed out") {
- $smtp_response = "450 4.4.2 Local delivery to $mbxname timed out";
- } elsif ($@ =~ /too many hops/i) {
- $smtp_response = "550 5.4.6 Rejected delivery to mailbox $mbxname: $@";
- } else {
- $smtp_response = "451 4.5.0 Local delivery to mailbox $mbxname failed: $@";
- }
- } # end of block, 'last' within block brings us here
- do_log(-1, $smtp_response) if $smtp_response !~ /^2/;
- $smtp_response .= ", id=" . am_id();
- $r->recip_smtp_response($smtp_response); $r->recip_done(2);
- $r->recip_mbxname($mbxname) if $mbxname ne '' && $smtp_response =~ /^2/;
- }
- section_time('save-to-local-mailbox');
- }
- 1;
- #
- package Amavis::Out;
- use strict;
- use re 'taint';
- BEGIN {
- use Exporter ();
- use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.043';
- @ISA = qw(Exporter);
- @EXPORT = qw(&mail_dispatch);
- }
- use Errno qw(ENOENT EACCES);
- use IO::File qw(O_CREAT O_EXCL O_WRONLY);
- use IO::Wrap;
- use Net::Cmd;
- use Net::SMTP 2.24;
- # use Authen::SASL;
- use POSIX qw(WIFEXITED WIFSIGNALED WIFSTOPPED
- WEXITSTATUS WTERMSIG WSTOPSIG);
- BEGIN {
- import Amavis::Conf qw(:platform $DEBUG $QUARANTINEDIR
- $relayhost_is_client c cr ca);
- import Amavis::Util qw(untaint min max ll do_log debug_oneshot
- am_id snmp_count exit_status_str
- prolong_timer run_command_consumer);
- import Amavis::Timing qw(section_time);
- import Amavis::rfc2821_2822_Tools;
- import Amavis::Out::Local qw(mail_to_local_mailbox);
- import Amavis::Out::EditHeader;
- }
- # modify delivery method string if $relayhost_is_client and mail came in by TCP
- sub dynamic_destination($$) {
- my($method,$conn) = @_;
- my($client_ip) = !defined($conn) ? undef : $conn->client_ip;
- if ($client_ip ne '' && $method =~ /^smtp:/i) {
- my(@list); $list[0] = ''; my($j) = 0;
- for ($method =~ /\G \[ (?: \\. | [^\]\\] )* \] | " (?: \\. | [^"\\] )* "
- | : | [ \t]+ | [^:"\[ \t]+ | . /gcsx) { # real parsing
- if ($_ eq ':') { $list[++$j] = '' } else { $list[$j] .= $_ }
- };
- my($new_method); my($via,$relayhost,$relayhost_port) = @list;
- if ($relayhost_is_client) # compatibility: deprecated $relayhost_is_client
- { ($relayhost,$relayhost_port) = ('*','*') }
- $relayhost = "[$client_ip]" if $relayhost eq '*';
- $relayhost_port = $conn->socket_port+1 if $relayhost_port eq '*';
- $new_method = join(':', $via,$relayhost,$relayhost_port,@list[3..$#list]);
- if ($new_method ne $method) {
- do_log(3, "dynamic destination override: $method -> $new_method");
- $method = $new_method;
- }
- }
- $method;
- }
- sub mail_dispatch($$$$;$) {
- my($conn) = shift;
- my($msginfo,$initial_submission,$dsn_per_recip_capable,$filter) = @_;
- my($via) = $msginfo->delivery_method;
- if ($via =~ /^smtp:/i) {
- mail_via_smtp(dynamic_destination($via,$conn), @_);
- } elsif ($via =~ /^pipe:/i) {
- mail_via_pipe($via, @_);
- } elsif ($via =~ /^bsmtp:/i) {
- mail_via_bsmtp($via, @_);
- } elsif ($via =~ /^sql:/i) {
- $Amavis::extra_code_sql_quar && $Amavis::sql_storage
- or die "SQL quarantine code not enabled";
- Amavis::Out::SQL::Quarantine::mail_via_sql(
- $Amavis::sql_dataset_conn_storage, @_);
- } elsif ($via =~ /^local:/i) {
- # 'local:' is used by the quarantine code to relieve it
- # of the need to know which delivery method needs to be used.
- # Deliver first what is local (whatever does not contain '@')
- mail_to_local_mailbox($via, $msginfo, $initial_submission,
- sub { shift->recip_final_addr !~ /\@/ ? 1 : 0 });
- if (grep { !$_->recip_done } @{$msginfo->per_recip_data}) {
- my($nm) = c('notify_method'); # deliver the rest
- if ($nm =~ /^smtp:/i) { mail_via_smtp(dynamic_destination($nm,$conn),@_)}
- elsif ($nm =~ /^pipe:/i) { mail_via_pipe($nm, @_) }
- elsif ($nm =~ /^bsmtp:/i) { mail_via_bsmtp($nm, @_) }
- elsif ($nm =~ /^sql:/i) {
- $Amavis::extra_code_sql_quar && $Amavis::sql_storage
- or die "SQL quarantine code not enabled";
- Amavis::Out::SQL::Quarantine::mail_via_sql(
- $Amavis::sql_dataset_conn_storage, @_);
- }
- }
- }
- }
- #sub Net::Cmd::debug_print {
- # my($cmd,$out,$text) = @_;
- # do_log(0, "*** ".$cmd->debug_text($out,$text)) if $out;
- #}
- # simple OO wrapper around Net::SMTP::datasend to provide a method 'print'
- # and to buffer data, avoiding a bottleneck in Net::Cmd::datasend
- #
- sub new_smtp_data {
- my($class, $handle) = @_;
- bless { handle => $handle, buff => '' }, $class;
- }
- sub close { my($self) = shift; $self->flush }
- sub print {
- my($self) = shift; $self->{buff} .= join('',@_);
- $self->flush if length($self->{buff}) >= 16384;
- 1;
- }
- sub flush {
- my($self) = shift;
- if ($self->{buff} ne '') {
- $self->{handle}->datasend($self->{buff})
- or die "datasend timed out while sending buffered data\n";
- $self->{buff} = '';
- }
- 1;
- }
- # Send mail using SMTP - do multiple transactions if necessary
- # (e.g. due to '452 Too many recipients')
- #
- sub mail_via_smtp(@) {
- my($via,$msginfo,$initial_submission,$dsn_per_recip_capable,$filter) = @_;
- my($num_recips_undone) =
- scalar(grep { !$_->recip_done && (!$filter || &$filter($_)) }
- @{$msginfo->per_recip_data});
- while ($num_recips_undone > 0) {
- mail_via_smtp_single(@_); # send what we can in one transaction
- my($num_recips_undone_after) =
- scalar(grep { !$_->recip_done && (!$filter || &$filter($_)) }
- @{$msginfo->per_recip_data});
- if ($num_recips_undone_after >= $num_recips_undone) {
- do_log(-2, "TROUBLE: Number of recipients ($num_recips_undone_after) "
- . "not reduced in SMTP transaction, abandon the effort");
- last;
- }
- if ($num_recips_undone_after > 0) {
- do_log(1, sprintf("Sent to %s recipients via SMTP, %s still to go",
- $num_recips_undone - $num_recips_undone_after,
- $num_recips_undone_after));
- }
- $num_recips_undone = $num_recips_undone_after;
- }
- 1;
- }
- # Send mail using SMTP - single transaction
- # (e.g. forwarding original mail or sending notification)
- # May throw exception (die) if temporary failure (4xx) or other problem
- #
- sub mail_via_smtp_single(@) {
- my($via,$msginfo,$initial_submission,$dsn_per_recip_capable,$filter) = @_;
- my($which_section) = 'fwd_init';
- snmp_count('OutMsgs');
- local($1,$2,$3); # avoid Perl taint bug, still in 5.8.3
- $via =~ /^smtp: (?: \[ ([^\]]*) \] | ([^:]*) ) : ([^:]*) /six
- or die "Bad fwd method syntax: $via";
- my($relayhost, $relayhost_port) = ($1.$2, $3);
- my($mta_id) = sprintf("[%s]:%s", $relayhost, $relayhost_port);
- my($btype) = $msginfo->body_type;
- if (!defined $btype || uc($btype) eq '7BIT') { $btype = '' }
- my($logmsg) = sprintf("%s via SMTP: %s", ($initial_submission?'SEND':'FWD'),
- qquote_rfc2821_local($msginfo->sender) );
- my(@per_recip_data) = grep { !$_->recip_done && (!$filter || &$filter($_)) }
- @{$msginfo->per_recip_data};
- if (!@per_recip_data) { do_log(5, "$logmsg, nothing to do"); return 1 }
- ll(4) && do_log(4, "(about to connect to $mta_id) $logmsg -> " .
- qquote_rfc2821_local(map {$_->recip_final_addr} @per_recip_data));
- my($msg) = $msginfo->mail_text; # a file handle or a MIME::Entity object
- my($smtp_handle, $smtp_response); my($smtp_code, $smtp_msg, $received_cnt);
- my($any_valid_recips) = 0; my($any_tempfail_recips) = 0;
- my($any_valid_recips_and_data_sent) = 0; my($in_datasend_mode) = 0;
- if (defined($msg) && !$msg->isa('MIME::Entity')) {
- # at this point, we have no idea what the user gave us...
- # a globref? a FileHandle?
- $msg = IO::Wrap::wraphandle($msg); # now we have an IO::Handle-like obj
- $msg->seek(0,0) or die "Can't rewind mail file: $!";
- }
- # NOTE: Net::SMTP uses alarm to do its own timing.
- # We need to restart our timer when Net::SMTP is done using it !!!
- my($remaining_time) = alarm(0); # check how much time is left, stop timer
- eval {
- $which_section = 'fwd-connect';
- # Timeout should be more than MTA normally takes to check DNS and RBL,
- # which may take a minute or more in case of unreachable DNS server.
- # Specifying shorter timeout will cause alarm to terminate the wait
- # for SMTP status line prematurely, resulting in status code 000.
- # rfc2821 (section 4.5.3.2) requires timeout to be at least 5 minutes
- my($localaddr) = c('local_client_bind_address'); # IP assigned to socket
- my($heloname) = c('localhost_name'); # host name used in HELO/EHLO
- $! = 0; $@ = undef; # seems like Net::SMTP puts its error status in $@
- $smtp_handle = Net::SMTP->new($relayhost, Port => $relayhost_port,
- ($localaddr eq '' ? () : (LocalAddr => $localaddr)),
- ($heloname eq '' ? () : (Hello => $heloname)),
- ExactAddresses => 1,
- Timeout => max(60, min(5 * 60, $remaining_time)), # for each operation
- # Timeout => 0, # no timeouts, disable nonblocking mode on socket
- # Debug => debug_oneshot(),
- );
- defined($smtp_handle) # don't change die text, it is referred to later
- or die "Can't connect to $relayhost port $relayhost_port, $@ ($!)";
- ll(5) && do_log(5,"Remote host presents itself as: ".$smtp_handle->domain);
- section_time($which_section);
- prolong_timer($which_section, $remaining_time); # restart timer
- $remaining_time = undef;
- $which_section = 'fwd-xforward';
- if ($msginfo->client_addr ne '' && $smtp_handle->supports('XFORWARD')) {
- my($cmd) = join(' ', 'XFORWARD', map
- { my($n,$v) = @$_;
- # may encode value as xtext/rfc3461 in future attributes:
- # char between "!" (33) and "~" (126) inclusive, except "+" and "="
- # $v =~ s/[^\041-\052\054-\074\076-\176]/sprintf("+%02X",ord($&))/eg;
- # Wietse says not to xtext-encode these four attrs, just neuter them
- $v =~ s/[^\041-\176]/?/g;
- $v =~ s/[<>()\\";@]/?/g; # other chars that are special in headers
- # postfix/smtpd/smtpd.c NEUTER_CHARACTERS (but ':' for IPv6)
- $v = substr($v,0,255) if length($v) > 255; # see XFORWARD_README
- $v eq '' ? () : ("$n=$v") }
- ( ['ADDR', $msginfo->client_addr], ['NAME',$msginfo->client_name],
- ['PROTO',$msginfo->client_proto],['HELO',$msginfo->client_helo] ));
- do_log(5, "sending $cmd");
- $smtp_handle->command($cmd);
- $smtp_handle->response()==2 or die "sending $cmd\n";
- section_time($which_section); prolong_timer($which_section);
- }
- $which_section = 'fwd-auth';
- my($auth_user) = $msginfo->auth_user;
- my($mechanisms) = $smtp_handle->supports('AUTH');
- if (!c('auth_required_out')) {
- do_log(3,"AUTH not needed, user='$auth_user', MTA offers '$mechanisms'");
- } elsif ($mechanisms eq '') {
- do_log(3,"INFO: MTA does not offer AUTH capability, user='$auth_user'");
- } elsif (!defined $auth_user) {
- do_log(0,"INFO: AUTH needed for submission but AUTH data not available");
- } else {
- do_log(3,"INFO: authenticating $auth_user, server supports AUTH $mechanisms");
- my($sasl) = Authen::SASL->new(
- 'callback' => { 'user' => $auth_user, 'authname' => $auth_user,
- 'pass' => $msginfo->auth_pass });
- $smtp_handle->auth($sasl) or die "sending AUTH, user=$auth_user\n";
- section_time($which_section); prolong_timer($which_section);
- }
- $which_section = 'fwd-mail-from';
- # how to pass the $msginfo->auth_submitter ???!!!
- $smtp_handle->mail(qquote_rfc2821_local($msginfo->sender),
- uc($btype) eq '8BITMIME' ? (Bits=>'8') : () )
- or die "sending MAIL FROM\n";
- section_time($which_section); prolong_timer($which_section);
- $which_section = 'fwd-rcpt-to';
- my($skipping_resp);
- for my $r (@per_recip_data) { # send recipient addresses
- if (defined $skipping_resp) {
- $r->recip_smtp_response($skipping_resp); $r->recip_done(2);
- next;
- }
- # send a RCPT TO command and get the response
- my($raddr) = qquote_rfc2821_local($r->recip_final_addr);
- $smtp_handle->recipient($raddr);
- $smtp_code = $smtp_handle->code;
- $smtp_msg = $smtp_handle->message;
- chomp($smtp_msg);
- my($rcpt_smtp_resp) = "$smtp_code $smtp_msg";
- if ($smtp_code =~ /^2/) {
- $any_valid_recips++;
- do_log(3, "response to RCPT TO for $raddr: \"$rcpt_smtp_resp\"");
- } else { # not ok
- do_log(1, "response to RCPT TO for $raddr: \"$rcpt_smtp_resp\"");
- if ($rcpt_smtp_resp =~ /^0/) {
- # timeout, what to do, could cause duplicates
- do_log(-1, "response to RCPT TO not yet available");
- $rcpt_smtp_resp = "450 4.4.2 ($rcpt_smtp_resp - probably timed out)";
- }
- $r->recip_remote_mta($relayhost);
- $r->recip_remote_mta_smtp_response($rcpt_smtp_resp);
- if ($rcpt_smtp_resp =~ /^ (\d{3}) \s+ ([245] \. \d{1,3} \. \d{1,3})?
- \s* (.*) \z/xs)
- {
- my($resp_code, $resp_enhcode, $resp_msg) = ($1, $2, $3);
- if ($resp_enhcode eq '' && $resp_code =~ /^([245])/) {
- my($c1) = $1;
- $resp_enhcode = $resp_code eq '452' ? "$c1.5.3" : "$c1.1.0";
- }
- $rcpt_smtp_resp = sprintf("%s %s %s, id=%s, from MTA(%s): %s",
- $resp_code, $resp_enhcode,
- ($resp_code=~/^2/ ? 'Ok' : 'Failed'),
- am_id(), $mta_id, $rcpt_smtp_resp);
- }
- if ($rcpt_smtp_resp =~ /^452/) { # too many recipients - see rfc2821
- do_log(-1, sprintf('Only %d recips sent in one go: "%s"',
- $any_valid_recips, $rcpt_smtp_resp));
- $skipping_resp = $rcpt_smtp_resp;
- } elsif ($rcpt_smtp_resp =~ /^4/) {
- $any_tempfail_recips++;
- $smtp_response = $rcpt_smtp_resp if !defined($smtp_response);
- }
- $r->recip_smtp_response($rcpt_smtp_resp); $r->recip_done(2);
- $smtp_response = $rcpt_smtp_resp
- if $rcpt_smtp_resp =~ /^5/ && $smtp_response !~ /^5/; # keep first 5x
- }
- }
- section_time($which_section); prolong_timer($which_section);
- $smtp_code = $smtp_msg = undef;
- if (!$any_valid_recips) {
- do_log(-1,"mail_via_smtp: DATA skipped, no valid recips, $any_tempfail_recips");
- } elsif ($any_tempfail_recips && !$dsn_per_recip_capable) {
- # we must not proceede if mail did not came in as LMTP,
- # or we would generate mail duplicates on each delivery attempt
- do_log(-1,"mail_via_smtp: DATA skipped, tempfailed recips: $any_tempfail_recips");
- } else { # send the message contents (enter DATA phase)
- $which_section = 'fwd-data';
- $smtp_handle->data or die "sending DATA command\n";
- $in_datasend_mode = 1;
- my($smtp_resp) = $smtp_handle->code . " " . $smtp_handle->message;
- chomp($smtp_resp);
- do_log(4, "response to DATA: \"$smtp_resp\"");
- # provide OO wrapper and buffering around Net::Cmd::datasend
- my($smtp_data_fh) = Amavis::Out->new_smtp_data($smtp_handle);
- my($hdr_edits) = $msginfo->header_edits;
- $hdr_edits = Amavis::Out::EditHeader->new if !$hdr_edits;
- $received_cnt = $hdr_edits->write_header($msg, $smtp_data_fh);
- if ($received_cnt > 100) {
- # loop detection required by rfc2821 6.2
- # Do not modify the signal text, it gets matched elsewhere!
- die "Too many hops: $received_cnt 'Received:' header lines\n";
- }
- if (!defined($msg)) {
- # empty mail body
- } elsif ($msg->isa('MIME::Entity')) {
- warn "---------------------------------------------------------";
- warn $msg->stringify;
- $msg->print_body($smtp_data_fh);
- } else {
- my($nbytes,$buff);
- # Using fixed-size reads instead of line-by-line approach
- # makes feeding mail back to MTA (e.g. Postfix) more than
- # twice as fast for larger mail.
- ### # to reduce likelyhood of a qmail bare-LF bug (bare LF reported when
- ### # CR and LF are separated by a TCP packet boundary) one may use this
- ### # 'while' loop, reading line by line, instead of the normal one below
- ### for (undef $!; defined($buff=$msg->getline); undef $!) {
- ### $smtp_handle->datasend($buff)
- ### or die "datasend timed out while sending body";
- ### }
- ### defined $buff || $!==0 or die "Error reading: $!";
- # must flush buffering through $smtp_data_fh, as from now on
- # we'll be calling Net::Cmd::datasend directly for speed
- $smtp_data_fh->flush or die "Error flushing smtp_data_fh: $!";
- while (($nbytes=$msg->read($buff,16384)) > 0) {
- $smtp_handle->datasend($buff)
- or die "datasend timed out while sending body";
- }
- defined $nbytes or die "Error reading: $!";
- }
- $smtp_data_fh->close or die "Error closing smtp_data_fh: $!";
- $smtp_data_fh = undef;
- section_time($which_section); prolong_timer($which_section);
- $which_section = 'fwd-data-end';
- # don't check status of dataend here, it may not yet be available
- $smtp_handle->dataend;
- $in_datasend_mode = 0; $any_valid_recips_and_data_sent = 1;
- section_time($which_section); prolong_timer($which_section);
- $which_section = 'fwd-rundown-1';
- # figure out the final SMTP response
- $smtp_code = $smtp_handle->code;
- my(@msgs) = $smtp_handle->message;
- # only the 'command()' resets messages list, so now we have both:
- # 'End data with <CR><LF>.<CR><LF>' and 'Ok: queued as...' in @msgs
- # and only the last SMTP response code in $smtp_handle->code
- my($smtp_msg) = $msgs[$#msgs]; chomp($smtp_msg); # take the last one
- $smtp_response = "$smtp_code $smtp_msg";
- do_log(4, "response to data end: \"$smtp_response\"");
- # credativ -jw
- $smtp_response =~ /queued as (.*)$/;
- do_log(0, "new postfix id: $1");
- # credativ end
- # replace success responses to RCPT TO commands with a final response
- for my $r (@per_recip_data) {
- next if $r->recip_done; # skip those that failed at RCPT TO
- $r->recip_remote_mta($relayhost);
- $r->recip_remote_mta_smtp_response($smtp_response);
- }
- }
- };
- my($err) = $@;
- my($saved_section_name) = $which_section;
- if ($err ne '') { chomp($err); $err = ' ' if $err eq '' } # careful chomp
- prolong_timer($which_section, $remaining_time); # restart the timer
- $which_section = 'fwd-rundown';
- if ($err ne '') { # fetch info about failure
- do_log(3, "mail_via_smtp: session failed: $err");
- if (!defined($smtp_handle)) { $smtp_code = ''; $smtp_msg = '' }
- else {
- $smtp_code = $smtp_handle->code; $smtp_msg = $smtp_handle->message;
- chomp($smtp_msg);
- }
- }
- # terminate the SMTP session if still alive
- if (!defined $smtp_handle) {
- # nothing
- } elsif ($in_datasend_mode) {
- # We are aborting SMTP session. DATA send mode must NOT be normally
- # terminated with a dataend (dot), otherwise recipient will receive
- # a chopped-off mail (and possibly be receiving it over and over again
- # during each MTA retry.
- do_log(-1, "mail_via_smtp: NOTICE: aborting SMTP session, $err");
- $smtp_handle->close; # abruptly terminate the SMTP session, ignoring status
- } else {
- $smtp_handle->timeout(15); # don't wait too long for response to a QUIT
- $smtp_handle->quit; # send a QUIT regardless of success so far
- if ($err eq '' && $smtp_handle->status != CMD_OK) {
- do_log(-1,"WARN: sending SMTP QUIT command failed: "
- . $smtp_handle->code . " " . $smtp_handle->message);
- }
- }
- # prepare final smtp response and log abnormal events
- if ($err eq '') { # no errors
- if ($any_valid_recips_and_data_sent && $smtp_response !~ /^[245]/) {
- $smtp_response =
- sprintf("451 4.6.0 Bad SMTP code, id=%s, from MTA(%s): %s",
- am_id(), $mta_id, $smtp_response);
- } elsif ($smtp_response =~ /^((\d)\d{2})/) {
- my($smtp_code,$smtp_status) = ($1,$2);
- $smtp_response = sprintf("%s %d.6.0 %s, id=%s, from MTA(%s): %s",
- $smtp_code, $smtp_status, ($smtp_status == 2 ? 'Ok' : 'Failed'),
- am_id(), $mta_id, $smtp_response);
- }
- } elsif ($err eq "timed out" || $err =~ /: Timeout\z/) {
- my($msg) = ($in_datasend_mode && $smtp_code =~ /^354/) ?
- '' : ", $smtp_code $smtp_msg";
- $smtp_response = sprintf("450 4.4.2 Timed out during %s%s, MTA(%s), id=%s",
- $saved_section_name, $msg, $mta_id, am_id());
- } elsif ($err =~ /^Can't connect/) {
- $smtp_response = sprintf("450 4.4.1 %s, MTA(%s), id=%s",
- $err, $mta_id, am_id());
- } elsif ($err =~ /^Too many hops/) {
- $smtp_response = sprintf("550 5.4.6 Rejected: %s, id=%s", $err, am_id());
- } elsif ($smtp_code =~ /^5/) { # 5xx
- $smtp_response = sprintf("%s 5.5.0 Rejected by MTA(%s): %s %s, id=%s",
- ($smtp_code !~ /^5\d\d\z/ ? "550" : $smtp_code),
- $mta_id, $smtp_code, $smtp_msg, am_id());
- } elsif ($smtp_code =~ /^0/) { # 000
- $smtp_response = sprintf("450 4.4.2 No response from MTA(%s) during %s (%s), id=%s",
- $mta_id, $saved_section_name, $err, am_id());
- } else {
- $smtp_response = sprintf("%s 4.5.0 From MTA(%s) during %s (%s): %s %s, id=%s",
- ($smtp_code !~ /^4\d\d\z/ ? "451" : $smtp_code),
- $mta_id, $saved_section_name, $err,
- $smtp_code, $smtp_msg, am_id());
- }
- do_log( ($smtp_response =~ /^2/ ? 1 : -1), $logmsg . " -> " .
- qquote_rfc2821_local(map {$_->recip_final_addr} @per_recip_data) .
- ", " . ($btype ne '' ? "BODY=$btype, " : '') . $smtp_response);
- if (defined $smtp_response) {
- for my $r (@per_recip_data) {
- if (!$r->recip_done) { # mark it as done
- $r->recip_smtp_response($smtp_response); $r->recip_done(2);
- $r->recip_mbxname($r->recip_final_addr) if $smtp_response =~ /^2/;
- } elsif ($any_valid_recips_and_data_sent
- && $r->recip_smtp_response =~ /^452/) {
- # 'undo' the RCPT TO '452 Too many recipients' situation,
- # needs to be handled in more than one transaction
- $r->recip_smtp_response(undef); $r->recip_done(undef);
- }
- }
- }
- if ( $smtp_response =~ /^2/) { snmp_count('OutMsgsDelivers') }
- elsif ($smtp_response =~ /^4/) { snmp_count('OutAttemptFails') }
- elsif ($smtp_response =~ /^5/) { snmp_count('OutMsgsRejects') }
- section_time($which_section);
- 1;
- }
- # Send mail using external mail submission program 'sendmail' (also available
- # with Postfix and Exim) - used for forwarding original mail or sending notif.
- # May throw exception (die) if temporary failure (4xx) or other problem
- #
- sub mail_via_pipe(@) {
- my($via,$msginfo,$initial_submission,$dsn_per_recip_capable,$filter) = @_;
- snmp_count('OutMsgs');
- $via =~ /^pipe:(.*)\z/si or die "Bad fwd method syntax: $via";
- my($pipe_args) = $1;
- $pipe_args =~ s/^flags=\S*\s*//i; # flags are currently ignored, q implied
- $pipe_args =~ s/^argv=//i;
- my(@per_recip_data) = grep { !$_->recip_done && (!$filter || &$filter($_)) }
- @{$msginfo->per_recip_data};
- my($logmsg) = sprintf("%s via PIPE: %s", ($initial_submission?'SEND':'FWD'),
- qquote_rfc2821_local($msginfo->sender));
- if (!@per_recip_data) {
- do_log(5, "$logmsg, nothing to do");
- return 1;
- }
- do_log(1, $logmsg . " -> " .
- qquote_rfc2821_local(map {$_->recip_final_addr} @per_recip_data));
- my($msg) = $msginfo->mail_text; # a file handle or a MIME::Entity object
- if (defined($msg) && !$msg->isa('MIME::Entity')) {
- $msg = IO::Wrap::wraphandle($msg); # now we have an IO::Handle-like obj
- $msg->seek(0,0) or die "Can't rewind mail file: $!";
- }
- my(@pipe_args) = split(' ', $pipe_args); my(@command) = shift @pipe_args;
- for (@pipe_args) {
- # The sendmail command line expects addresses quoted as per RFC 822.
- # "funny user"@some.domain
- # For compatibility with Sendmail, the Postfix sendmail command line
- # also accepts address formats that are legal in RFC 822 mail headers:
- # Funny Dude <"funny user"@some.domain>
- # Although addresses passed as args to sendmail initial submission
- # should not be <...> bracketed, for some reason original sendmail
- # issues a warning on null reverse-path, but gladly accepty <>.
- # As this is not strictly wrong, we comply to make it happy.
- # NOTE: the -fsender is not allowed, -f and sender must be separate args!
- if (/^\$\{sender\}\z/i) {
- push(@command,
- map { $_ eq '' ? '<>' : untaint(quote_rfc2821_local($_)) }
- $msginfo->sender);
- } elsif (/^\$\{recipient\}\z/i) {
- push(@command,
- map { $_ eq '' ? '<>' : untaint(quote_rfc2821_local($_)) }
- map { $_->recip_final_addr } @per_recip_data);
- } else {
- push(@command, $_);
- }
- }
- do_log(5, "mail_via_pipe running command: " . join(' ', @command));
- local $SIG{CHLD} = 'DEFAULT';
- local $SIG{PIPE} = 'IGNORE'; # write to broken pipe would throw a signal
- my($mp,$pid) = run_command_consumer(undef,undef,@command);
- binmode($mp) or die "Can't set pipe to binmode: $!"; # dflt since Perl 5.8.1
- my($hdr_edits) = $msginfo->header_edits;
- $hdr_edits = Amavis::Out::EditHeader->new if !$hdr_edits;
- my($received_cnt) = $hdr_edits->write_header($msg, $mp);
- if ($received_cnt > 100) { # loop detection required by rfc2821 6.2
- # deal with it later, for now just skip the body
- } elsif (!defined($msg)) {
- # empty mail body
- } elsif ($msg->isa('MIME::Entity')) {
- $msg->print_body($mp);
- } else {
- my($nbytes,$buff);
- while (($nbytes=$msg->read($buff,16384)) > 0)
- { $mp->print($buff) or die "Submitting mail text failed: $!" }
- defined $nbytes or die "Error reading: $!";
- }
- my($smtp_response);
- if ($received_cnt > 100) { # loop detection required by rfc2821 6.2
- do_log(-2, "Too many hops: $received_cnt 'Received:' header lines");
- kill('TERM',$pid); # kill the process running mail submission program
- $mp->close; # and ignore status
- $smtp_response = "550 5.4.6 Rejected: " .
- "Too many hops: $received_cnt 'Received:' header lines";
- } else {
- my($err); $mp->close or $err=$!; my($child_stat) = $?;
- my($error_str) = exit_status_str($child_stat,$err);
- my($status) = WEXITSTATUS($child_stat);
- # sendmail program (Postfix variant) can return the following exit codes:
- # EX_OK(0), EX_DATAERR, EX_SOFTWARE, EX_TEMPFAIL, EX_NOUSER, EX_UNAVAILABLE
- if ($status == EX_OK) {
- $smtp_response = "250 2.6.0 Ok"; # submitted to MTA
- snmp_count('OutMsgsDelivers');
- } elsif ($status == EX_TEMPFAIL) {
- $smtp_response = "450 4.5.0 Temporary failure submitting message";
- snmp_count('OutAttemptFails');
- } elsif ($status == EX_NOUSER) {
- $smtp_response = "550 5.1.1 Recipient unknown";
- snmp_count('OutMsgsRejects');
- } elsif ($status == EX_UNAVAILABLE) {
- $smtp_response = "550 5.5.0 Mail submission service unavailable";
- snmp_count('OutMsgsRejects');
- } else {
- $smtp_response = "451 4.5.0 Failed to submit a message: $error_str";
- snmp_count('OutAttemptFails');
- }
- }
- $smtp_response .= ", id=" . am_id();
- for my $r (@per_recip_data) {
- next if $r->recip_done;
- $r->recip_smtp_response($smtp_response); $r->recip_done(2);
- $r->recip_mbxname($r->recip_final_addr) if $smtp_response =~ /^2/;
- }
- section_time('fwd-pipe');
- 1;
- }
- sub mail_via_bsmtp(@) {
- my($via,$msginfo,$initial_submission,$dsn_per_recip_capable,$filter) = @_;
- snmp_count('OutMsgs'); local($1);
- $via =~ /^bsmtp:(.*)\z/si or die "Bad fwd method: $via";
- my($bsmtp_file_final) = $1; my($mbxname);
- my($s) = $msginfo->sender; # defanged sender name for use in filename
- $s =~ tr/a-zA-Z0-9@._+-]/=/c;
- $s = substr($s,0,100)."..." if length($s) > 100+3;
- $s =~ s/\@/_at_/g; $s =~ s/^(\.{0,2})\z/_$1/g;
- $bsmtp_file_final =~ s{%(.)}
- { $1 eq 'b' ? $msginfo->body_digest
- : $1 eq 'm' ? $msginfo->mail_id
- : $1 eq 's' ? untaint($s)
- : $1 eq 'i' ? iso8601_timestamp($msginfo->rx_time,1,'-')
- : $1 eq 'n' ? am_id()
- : $1 eq '%' ? '%' : '%'.$1 }egs;
- # prepend directory if not specified
- $bsmtp_file_final = $QUARANTINEDIR."/".$bsmtp_file_final
- if $QUARANTINEDIR ne '' && $bsmtp_file_final !~ m{^/};
- my($bsmtp_file_tmp) = $bsmtp_file_final . ".tmp";
- my(@per_recip_data) = grep { !$_->recip_done && (!$filter || &$filter($_)) }
- @{$msginfo->per_recip_data};
- my($logmsg) = sprintf("%s via BSMTP: %s", ($initial_submission?'SEND':'FWD'),
- qquote_rfc2821_local($msginfo->sender));
- if (!@per_recip_data) { do_log(5, "$logmsg, nothing to do"); return 1 }
- do_log(1, $logmsg . " -> " .
- qquote_rfc2821_local(map {$_->recip_final_addr} @per_recip_data) .
- ", file " . $bsmtp_file_final);
- my($msg) = $msginfo->mail_text; # a scalar reference, or a file handle
- if (defined($msg) && !$msg->isa('MIME::Entity')) {
- $msg = IO::Wrap::wraphandle($msg); # now we have an IO::Handle-like obj
- $msg->seek(0,0) or die "Can't rewind mail file: $!";
- }
- my($mp);
- eval {
- my($errn) = stat($bsmtp_file_tmp) ? 0 : 0+$!;
- if ($errn == ENOENT) {} # good, no file, as expected
- elsif (!$errn && -f _)
- { die "File $bsmtp_file_tmp already exists, refuse to overwrite" }
- else
- { die "File $bsmtp_file_tmp exists??? Refuse to overwrite it, $!" }
- $mp = IO::File->new;
- $mp->open($bsmtp_file_tmp, O_CREAT|O_EXCL|O_WRONLY, 0640)
- or die "Can't create BSMTP file $bsmtp_file_tmp: $!";
- binmode($mp, ":bytes") or die "Can't set :bytes, $!" if $unicode_aware;
- $mp->print("EHLO ", c('localhost_name'), $eol)
- or die "print failed (EHLO): $!";
- my($btype) = $msginfo->body_type;
- if (!defined $btype || uc($btype) eq '7BIT') { $btype = '' }
- $mp->printf("MAIL FROM:%s%s%s", # rfc1652: need "8bit Data"? (rfc2045)
- qquote_rfc2821_local($msginfo->sender),
- $btype ne '' ? ' BODY='.uc($btype) : '', $eol)
- or die "print failed (MAIL FROM): $!";
- for my $r (@per_recip_data) {
- $mp->print("RCPT TO:", qquote_rfc2821_local($r->recip_final_addr), $eol)
- or die "print failed (RCPT TO): $!";
- }
- $mp->print("DATA", $eol) or die "print failed (DATA): $!";
- my($hdr_edits) = $msginfo->header_edits;
- $hdr_edits = Amavis::Out::EditHeader->new if !$hdr_edits;
- my($received_cnt) = $hdr_edits->write_header($msg,$mp);
- if ($received_cnt > 100) { # loop detection required by rfc2821 6.2
- die "Too many hops: $received_cnt 'Received:' header lines";
- } elsif (!defined($msg)) { # empty mail body
- } elsif ($msg->isa('MIME::Entity')) {
- $msg->print_body($mp);
- } else {
- my($ln);
- for (undef $!; defined($ln=$msg->getline); undef $!) {
- $mp->print($ln=~/^\./ ?(".",$ln) :$ln) or die "print failed-data: $!";
- }
- defined $ln || $!==0 or die "Error reading: $!";
- }
- $mp->print(".", $eol) or die "print failed (final dot): $!";
- # $mp->print("QUIT",$eol) or die "print failed (QUIT): $!";
- $mp->close or die "Error closing BSMTP file $bsmtp_file_tmp: $!";
- $mp = undef;
- rename($bsmtp_file_tmp, $bsmtp_file_final)
- or die "Can't rename BSMTP file to $bsmtp_file_final: $!";
- $mbxname = $bsmtp_file_final;
- };
- my($err) = $@; my($smtp_response);
- if ($err eq '') {
- $smtp_response = "250 2.6.0 Ok, queued as BSMTP $bsmtp_file_final";
- snmp_count('OutMsgsDelivers');
- } else {
- chomp($err);
- unlink($bsmtp_file_tmp)
- or do_log(-2,"Can't delete half-finished BSMTP file $bsmtp_file_tmp: $!");
- $mp->close if defined $mp; # ignore status
- if ($err =~ /too many hops/i) {
- $smtp_response = "550 5.4.6 Rejected: $err";
- snmp_count('OutMsgsRejects');
- } else {
- $smtp_response = "451 4.5.0 Writing $bsmtp_file_tmp failed: $err";
- snmp_count('OutAttemptFails');
- }
- }
- $smtp_response .= ", id=" . am_id();
- for my $r (@per_recip_data) {
- next if $r->recip_done;
- $r->recip_smtp_response($smtp_response); $r->recip_done(2);
- $r->recip_mbxname($mbxname) if $mbxname ne '' && $smtp_response =~ /^2/;
- }
- section_time('fwd-bsmtp');
- 1;
- }
- 1;
- #
- package Amavis::UnmangleSender;
- use strict;
- use re 'taint';
- BEGIN {
- use Exporter ();
- use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.043';
- @ISA = qw(Exporter);
- @EXPORT_OK = qw(&best_try_originator_ip &best_try_originator
- &first_received_from);
- }
- use subs @EXPORT_OK;
- BEGIN {
- import Amavis::Conf qw(:platform @viruses_that_fake_sender_maps);
- import Amavis::Util qw(ll do_log);
- import Amavis::rfc2821_2822_Tools qw(
- split_address parse_received fish_out_ip_from_received);
- import Amavis::Lookup qw(lookup);
- import Amavis::Lookup::IP qw(lookup_ip_acl);
- }
- use Mail::Address;
- # Returns the envelope sender address, or reconstructs it if there is
- # a good reason to believe the envelope address has been changed or forged,
- # as is common for some varieties of viruses. Returns best guess of the
- # sender address, or undef if it can not be determined.
- #
- sub unmangle_sender($$$) {
- my($sender) = shift; # rfc2821 envelope sender address
- my($from) = shift; # rfc2822 'From:' header, may include comment
- my($virusname_list) = shift; # list ref containing names of detected viruses
- # based on ideas from Furio Ercolessi, Mike Atkinson, Mark Martinec
- # my($localpart,$domain) = split_address($sender);
- # # extract the RFC2822 'from' address, ignoring phrase and comment
- # chomp($from);
- # { local($1,$2,$3,$4); # avoid Perl 5.8.0 & 5.8.2 bug, $1 gets tainted !
- # $from = (Mail::Address->parse($from))[0];
- # }
- # $from = $from->address if $from ne '';
- # # NOTE: rfc2822 allows multiple addresses in the From field!
- my($best_try_originator) = $sender;
- if ($best_try_originator ne '') {
- for my $vn (@$virusname_list) {
- my($result,$matching_key) = lookup(0,$vn,@viruses_that_fake_sender_maps);
- if ($result) {
- do_log(2, "Virus $vn matches $matching_key, sender addr ignored");
- $best_try_originator = undef; last;
- }
- }
- }
- $best_try_originator;
- }
- # Given a dotted-quad IPv4 address try reverse DNS resolve, and then
- # forward DNS resolve. If they match, return domain name,
- # otherwise return the IP address in brackets. (resolves IPv4 only)
- #
- sub ip_addr_to_name($) {
- my($addr) = @_; # dotted-quad address string
- local($1,$2,$3,$4); my($result);
- if ($addr !~ /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\z/) {
- $result = $addr; # not an IPv4 address
- } else {
- my($binaddr) = pack('C4', $1,$2,$3,$4); # to binary string
- do_log(5, "ip_addr_to_name: DNS reverse-resolving: $addr");
- my(@addr) = gethostbyaddr($binaddr,2); # IP -> name
- $result = '['.$addr.']'; # IP address in brackets if nothing matches
- if (@addr) {
- my($name,$aliases,$addrtype,$length,@addrs) = @addr;
- if ($name =~ /[^.]\.[a-zA-Z]+\z/s) {
- do_log(5, "ip_addr_to_name: DNS forward-resolving: $name");
- my(@raddr) = gethostbyname($name); # name -> IP
- my($rname,$raliases,$raddrtype,$rlength,@raddrs) = @raddr;
- for my $ra (@raddrs) {
- if (lc($ra) eq lc($binaddr)) { $result = $name; last }
- }
- }
- }
- }
- do_log(3, "ip_addr_to_name: returning: $result");
- $result;
- }
- # Obtain and parse the first entry (chronologically) in the 'Received:' header
- # path trace - to be used as the value of the macro %t in customized messages
- #
- sub first_received_from($) {
- my($entity) = shift;
- my($first_received);
- if (defined($entity)) {
- my($fields) = parse_received($entity->head->get('received', -1));
- if (exists $fields->{'from'}) {
- my($item, $v1, $v2, $v3, $comment) = @{$fields->{'from'}};
- $first_received = join(' ', $item, $comment);
- $first_received =~ s/^[ \t\n\r]+//s; # discard leading whitespace
- $first_received =~ s/[ \t\n\r]+\z//s; # discard trailing whitespace
- }
- do_log(5, "first_received_from: $first_received");
- }
- $first_received;
- }
- # Try to extract sender's public IP address from the Received trace
- #
- use vars qw(@publicnetworks_maps);
- sub best_try_originator_ip($) {
- my($entity) = @_;
- @publicnetworks_maps = (
- Amavis::Lookup::Label->new('publicnetworks'),
- Amavis::Lookup::IP->new(qw(
- !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
- !169.254.0.0/16 !192.0.2.0/24 !192.88.99.0/24 !224.0.0.0/4
- [::FFFF:0:0]/96 ![::] ![::1] ![FF00::]/8 ![FE80::]/10 ![FEC0::]/10
- [::]/0)) ) if !@publicnetworks_maps; # rfc3330, rfc3513
- my($first_received_from_ip);
- if (defined($entity)) {
- my(@received) = reverse $entity->head->get_all('received');
- $#received = 5 if $#received > 5; # first six, chronologically
- for my $r (@received) {
- $first_received_from_ip = fish_out_ip_from_received($r);
- if ($first_received_from_ip ne '') {
- my($is_public,$fullkey,$err) =
- lookup_ip_acl($first_received_from_ip,@publicnetworks_maps);
- last if (!defined($err) || $err eq '') && $is_public;
- }
- }
- do_log(5, "best_try_originator_ip: $first_received_from_ip");
- }
- $first_received_from_ip;
- }
- # For the purpose of informing administrators try to obtain true sender
- # address or at least its site, as most viruses and spam have a nasty habit
- # of faking envelope sender address. Return a pair of addresses:
- # - the first (if defined) appears valid and may be used for sender
- # notifications;
- # - the second should only be used in generating customizable notification
- # messages (macro %o), NOT to be used as address for sending notifications,
- # as it can contain invalid address (but can be more informative).
- #
- sub best_try_originator($$$) {
- my($sender, $entity, $virusname_list) = @_;
- my($from) = !defined($entity) ? '' : $entity->head->get('from',0);
- my($originator) = unmangle_sender($sender,$from,$virusname_list);
- return ($originator, $originator) if defined $originator;
- my($first_received_from_ip) = best_try_originator_ip($entity);
- $originator = '?@' . ip_addr_to_name($first_received_from_ip)
- if $first_received_from_ip ne '';
- (undef, $originator);
- }
- 1;
- #
- package Amavis::Unpackers::NewFilename;
- use strict;
- use re 'taint';
- BEGIN {
- use Exporter ();
- use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.043';
- @ISA = qw(Exporter);
- @EXPORT_OK = qw(&consumed_bytes);
- }
- BEGIN {
- import Amavis::Conf qw(c cr ca
- $MIN_EXPANSION_QUOTA $MIN_EXPANSION_FACTOR
- $MAX_EXPANSION_QUOTA $MAX_EXPANSION_FACTOR);
- import Amavis::Util qw(ll do_log min max);
- }
- use vars qw($avail_quota); # available bytes quota for unpacked mail
- use vars qw($rem_quota); # remaining bytes quota for unpacked mail
- sub new($;$$) { # create a file name generator object
- my($class, $maxfiles,$mail_size) = @_;
- # calculate and initialize quota
- $avail_quota = $rem_quota = # quota in bytes
- max($MIN_EXPANSION_QUOTA, $mail_size * $MIN_EXPANSION_FACTOR,
- min($MAX_EXPANSION_QUOTA, $mail_size * $MAX_EXPANSION_FACTOR));
- do_log(4,"Original mail size: $mail_size; quota set to: $avail_quota bytes");
- # create object
- bless {
- num_of_issued_names => 0, first_issued_ind => 1, last_issued_ind => 0,
- maxfiles => $maxfiles, # undef disables limit
- objlist => [],
- }, $class;
- }
- sub parts_list_reset($) { # clear a list of recently issued names
- my($self) = shift;
- $self->{num_of_issued_names} = 0;
- $self->{first_issued_ind} = $self->{last_issued_ind} + 1;
- $self->{objlist} = [];
- }
- sub parts_list($) { # returns a ref to a list of recently issued names
- my($self) = shift;
- $self->{objlist};
- }
- sub parts_list_add($$) { # add a parts object to the list of parts
- my($self, $part) = @_;
- push(@{$self->{objlist}}, $part);
- }
- sub generate_new_num($$) { # make-up a new number for a file and return it
- my($self, $ignore_limit) = @_;
- $ignore_limit = 0 if !defined($ignore_limit);
- if (!$ignore_limit && defined($self->{maxfiles}) &&
- $self->{num_of_issued_names} >= $self->{maxfiles}) {
- # do not change the text in die without adjusting decompose_part()
- die "Maximum number of files ($self->{maxfiles}) exceeded";
- }
- $self->{num_of_issued_names}++; $self->{last_issued_ind}++;
- $self->{last_issued_ind};
- }
- sub consumed_bytes($$;$$) {
- my($bytes, $bywhom, $tentatively, $exquota) = @_;
- my($perc) = !$avail_quota ? '' : sprintf(", (%.0f%%)",
- 100 * ($avail_quota - ($rem_quota - $bytes)) / $avail_quota);
- ll(4) && do_log(4,"Charging $bytes bytes to remaining quota $rem_quota"
- . " (out of $avail_quota$perc) - by $bywhom");
- if ($bytes > $rem_quota && $rem_quota >= 0) {
- # Do not modify the following signal text, it gets matched elsewhere!
- my($msg) = "Exceeded storage quota $avail_quota bytes by $bywhom; ".
- "last chunk $bytes bytes";
- do_log(-1, $msg);
- die "$msg\n" if !$exquota; # die, unless allowed to exceed quota
- }
- $rem_quota -= $bytes unless $tentatively;
- $rem_quota; # return remaining quota
- }
- 1;
- #
- package Amavis::Unpackers::Part;
- use strict;
- use re 'taint';
- BEGIN {
- use Exporter ();
- use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.043';
- @ISA = qw(Exporter);
- }
- BEGIN {
- import Amavis::Util qw(ll do_log);
- }
- use vars qw($file_generator_object);
- sub init($) { $file_generator_object = shift }
- sub new($;$$$) { # create a part descriptor object
- my($class, $dir_name,$parent,$ignore_limit) = @_;
- my($self) = bless {}, $class;
- if (!defined($dir_name) && !defined($parent)) {
- # just make an empty object, presumably used as a new root
- } else {
- $self->number($file_generator_object->generate_new_num($ignore_limit));
- $self->dir_name($dir_name) if defined $dir_name;
- if (defined $parent) {
- $self->parent($parent);
- my($ch_ref) = $parent->children;
- push(@$ch_ref,$self); $parent->children($ch_ref);
- }
- $file_generator_object->parts_list_add($self); # save it
- ll(4) && do_log(4, "Issued a new " .
- (defined $dir_name ? "file name" : "pseudo part") . ": " .
- $self->base_name);
- }
- $self;
- }
- sub number
- { my($self)=shift; !@_ ? $self->{number} : ($self->{number}=shift) };
- sub dir_name
- { my($self)=shift; !@_ ? $self->{dir_name} : ($self->{dir_name}=shift) };
- sub parent
- { my($self)=shift; !@_ ? $self->{parent} : ($self->{parent}=shift) };
- sub children
- { my($self)=shift; !@_ ? $self->{children}||[] : ($self->{children}=shift) };
- sub mime_placement # part location within a MIME tree, e.g. "1/1/3"
- { my($self)=shift; !@_ ? $self->{place} : ($self->{place}=shift) };
- sub type_short # string or a ref to a list of strings
- { my($self)=shift; !@_ ? $self->{ty_short} : ($self->{ty_short}=shift) };
- sub type_long
- { my($self)=shift; !@_ ? $self->{ty_long} : ($self->{ty_long}=shift) };
- sub type_declared
- { my($self)=shift; !@_ ? $self->{ty_decl} : ($self->{ty_decl}=shift) };
- sub name_declared # string or a ref to a list of strings
- { my($self)=shift; !@_ ? $self->{nm_decl} : ($self->{nm_decl}=shift) };
- sub size
- { my($self)=shift; !@_ ? $self->{size} : ($self->{size}=shift) };
- sub exists
- { my($self)=shift; !@_ ? $self->{exists} : ($self->{exists}=shift) };
- sub attributes # listref of characters representing attributes
- { my($self)=shift; !@_ ? $self->{attr} : ($self->{attr}=shift) };
- sub attributes_add { # U=undecodable, C=crypted, D=directory,S=special,L=link
- my($self)=shift; my($a) = $self->{attr} || [];
- for my $arg (@_) { push(@$a,$arg) if $arg ne '' && !grep {$_ eq $arg} @$a }
- $self->{attr} = $a;
- };
- sub base_name { my($self)=shift; sprintf("p%03d",$self->number) }
- sub full_name {
- my($self)=shift; my($d) = $self->dir_name;
- !defined($d) ? undef : $d.'/'.$self->base_name;
- }
- # returns a ref to a list of part ancestors, starting with the root object,
- # and including the part object itself
- sub path {
- my($self)=shift;
- my(@path);
- for (my($p)=$self; defined($p); $p=$p->parent) { unshift(@path,$p) }
- \@path;
- };
- 1;
- #
- package Amavis::Unpackers::OurFiler;
- use strict;
- use re 'taint';
- BEGIN {
- use Exporter ();
- use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.043';
- @ISA = qw(Exporter MIME::Parser::Filer); # subclass of MIME::Parser::Filer
- }
- # This package will be used by mime_decode().
- #
- # We don't want no heavy MIME::Parser machinery for file name extension
- # guessing, decoding charsets in filenames (and listening to complaints
- # about it), checking for evil filenames, checking for filename contention, ...
- # (which can not be turned off completely by ignore_filename(1) !!!)
- # Just enforce our file name! And while at it, collect generated filenames.
- #
- sub new($$$) {
- my($class, $dir, $parent_obj) = @_;
- $dir =~ s{/+\z}{}; # chop off trailing slashes from directory name
- bless {parent => $parent_obj, directory => $dir}, $class;
- }
- # provide a generated file name
- sub output_path($@) {
- my($self, $head) = @_;
- my($newpart_obj) =
- Amavis::Unpackers::Part->new($self->{directory}, $self->{parent}, 1);
- get_amavisd_part($head, $newpart_obj); # store object into head
- $newpart_obj->full_name;
- }
- sub get_amavisd_part($;$) {
- my($head) = shift;
- !@_ ? $head->{amavisd_parts_obj} : ($head->{amavisd_parts_obj} = shift);
- }
- 1;
- #
- package Amavis::Unpackers::Validity;
- use strict;
- use re 'taint';
- BEGIN {
- use Exporter ();
- use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.043';
- @ISA = qw(Exporter);
- @EXPORT_OK = qw(&check_header_validity &check_for_banned_names);
- }
- BEGIN {
- import Amavis::Util qw(ll do_log sanitize_str);
- import Amavis::Conf qw(:platform %banned_rules c cr ca);
- import Amavis::Lookup qw(lookup);
- }
- use subs @EXPORT_OK;
- sub check_header_validity($$) {
- my($conn, $msginfo) = @_;
- my(@bad); my($curr_head);
- for my $next_head (@{$msginfo->orig_header}, "\n") {
- if ($next_head =~ /^[ \t]/) { $curr_head .= $next_head } # folded
- else { # new header
- if (!defined($curr_head)) { # no previous complete header
- } else {
- # obsolete rfc822 syntax allowed whitespace before colon
- my($field_name, $field_body) =
- $curr_head =~ /^([!-9;-\176]+)[ \t]*:(.*)\z/s
- ? ($1, $2) : (undef, $curr_head);
- my($msg1,$msg2);
- if (!defined($field_name) && $curr_head=~/^()()(.*)\z/s) {
- $msg1 = "Invalid header field syntax";
- } elsif ($curr_head =~ /^(.*?)([\000\015])(.*)\z/s) {
- $msg1 = "Improper use of control character";
- } elsif ($curr_head =~ /^(.*?)([\200-\377])(.*)\z/s) {
- $msg1 = "Non-encoded 8-bit data";
- } elsif ($curr_head =~ /^(.*?)([^\000-\377])(.*)\z/s) {
- $msg1 = "Non-encoded Unicode character"; # should not happen
- } elsif ($curr_head =~ /^()()([ \t]+)$/m) {
- $msg1 ="Improper folded header field made up entirely of whitespace";
- }
- if (defined $msg1) {
- my($pre, $ch, $post) = ($1, $2, $3);
- if (length($post) > 20) { $post = substr($post,0,15) . "..." }
- if (length($pre)-length($field_name)-2 > 50-length($post)) {
- $pre = "$field_name: ..."
- . substr($pre, length($pre) - (45-length($post)));
- }
- $msg1 .= sprintf(" (char %02X hex)", ord($ch)) if length($ch)==1;
- $msg1 .= " in message header '$field_name'" if $field_name ne '';
- $msg2 = sanitize_str($pre); my($msg2_pre_l) = length($msg2);
- $msg2 .= sanitize_str($ch . $post);
- # push(@bad, "$msg1\n $msg2\n " . (' ' x $msg2_pre_l) . '^');
- push(@bad, "$msg1: $msg2");
- }
- }
- last if $next_head eq $eol; # end-of-header reached
- last if @bad >= 100; # some sanity limit
- $curr_head = $next_head;
- }
- }
- ll(5) && do_log(5,"check_header: ".(!@bad ? "OK" : join(', ',@bad)));
- @bad;
- }
- sub check_for_banned_names($$) {
- my($msginfo,$parts_root) = @_;
- do_log(3, "Checking for banned types and filenames");
- my($bypmr) = ca('bypass_banned_checks_maps');
- my($bfnmr) = ca('banned_filename_maps'); # two-level map: recip, partname
- my(@recip_tables); # a list of records describing banned tables for recips
- my($any_table_in_recip_tables) = 0; my($any_not_bypassed) = 0;
- for my $r (@{$msginfo->per_recip_data}) {
- my($recip) = $r->recip_addr;
- my(@tables,@tables_m); # list of banned lookup tables for this recipient
- if (!lookup(0,$recip,@$bypmr)) { # not bypassed
- $any_not_bypassed = 1;
- my($t_ref,$m_ref) = lookup(1,$recip,@$bfnmr);
- if (defined $t_ref) {
- for my $ti (0..$#$t_ref) { # collect all relevant tables for each recip
- my($t) = $t_ref->[$ti];
- # an entry may be a ref to a list of lookup tables, or a comma- or
- # whitespace-separated list of table names (suitable for SQL),
- # which are mapped to actual lookup tables through %banned_rules
- if (!defined($t)) { # ignore
- } elsif (ref($t) eq 'ARRAY') { # a list of actual lookup tables
- push(@tables, @$t);
- push(@tables_m, ($m_ref->[$ti]) x @$t);
- } else { # a list of rules _names_, to be mapped via %banned_rules
- my(@names); my(@rawnames) = grep { !/^[, ]*\z/ }
- ($t =~ /\G (?: " (?: \\. | [^"\\] )* " | [^, ] )+ | [, ]+/gcsx);
- # in principle the quoted strings could be used
- # to construct lookup tables on-the-fly (not implemented)
- for my $n (@rawnames) { # collect only valid names
- if (!exists($banned_rules{$n})) {
- do_log(2,"INFO: unknown banned table name $n, recip=$recip");
- } elsif (!defined($banned_rules{$n})) { # ignore undef
- } else { push(@names,$n) }
- }
- ll(3) && do_log(3,"collect banned table[$ti]: $recip, tables: ".
- join(', ', map { $_.'=>'.$banned_rules{$_} } @names));
- if (@names) { # any known and valid table names?
- push(@tables, map { $banned_rules{$_} } @names);
- push(@tables_m, ($m_ref->[$ti]) x @names);
- }
- }
- }
- }
- }
- push(@recip_tables, { r => $r, recip => $recip,
- tables => \@tables, tables_m => \@tables_m } );
- $any_table_in_recip_tables++ if @tables;
- }
- my($bnpre) = cr('banned_namepath_re');
- if (!$any_not_bypassed) {
- do_log(3,"skipping banned check: all recipients bypass banned checks");
- } elsif (!$any_table_in_recip_tables && !(ref $bnpre && ref $$bnpre)) {
- do_log(3,"skipping banned check: no applicable lookup tables");
- } else {
- do_log(4,"starting banned checks - traversing message structure tree");
- my($part);
- for (my(@unvisited)=($parts_root);
- @unvisited and $part=shift(@unvisited);
- push(@unvisited,@{$part->children}))
- { # traverse decomposed parts tree breadth-first
- my(@path) = @{$part->path};
- next if @path <= 1;
- shift(@path); # ignore place-holder root node
- next if @{$part->children}; # ignore non-leaf nodes
- my(@descr_trad); # a part path: list of predecessors of a message part
- my(@descr); # same, but in form suitable for check on banned_namepath_re
- for my $p (@path) {
- my(@k,$n);
- $n = $p->base_name;
- if ($n ne '') { $n=~s/[\t\n]/ /g; push(@k,"P=$n") }
- $n = $p->mime_placement;
- if ($n ne '') { $n=~s/[\t\n]/ /g; push(@k,"L=$n") }
- $n = $p->type_declared;
- $n = [$n] if !ref($n);
- for (@$n) {if ($_ ne ''){my($m)=$_; $m=~s/[\t\n]/ /g; push(@k,"M=$m")}}
- $n = $p->type_short;
- $n = [$n] if !ref($n);
- for (@$n) {if (defined($_) && $_ ne '')
- {my($m)=$_; $m=~s/[\t\n]/ /g; push(@k,"T=$m")} }
- $n = $p->name_declared;
- $n = [$n] if !ref($n);
- for (@$n) {if (defined($_) && $_ ne '')
- {my($m)=$_; $m=~s/[\t\n]/ /g; push(@k,"N=$m")} }
- $n = $p->attributes;
- $n = [$n] if !ref($n);
- for (@$n) {if (defined($_) && $_ ne '')
- {my($m)=$_; $m=~s/[\t\n]/ /g; push(@k,"A=$m")} }
- push(@descr, join("\t",@k));
- push(@descr_trad, [map { local($1,$2);
- /^([a-zA-Z0-9])=(.*)\z/s; my($key_what,$key_val) = ($1,$2);
- $key_what eq 'M' || $key_what eq 'N' ? $key_val
- : $key_what eq 'T' ? ('.'.$key_val) # prepend a dot (compatibility)
- : $key_what eq 'A' && $key_val eq 'U' ? 'UNDECIPHERABLE' : ()} @k]);
- }
- # we have obtained a description of a part as a list of its predecessors
- # in a message structure including the part itself at the end of the list
- my($key_val_str) = join(' | ',@descr); $key_val_str =~ s/\t/,/g;
- my($key_val_trad_str) = join(' | ', map {join(',',@$_)} @descr_trad);
- # evaluate current mail component path against each recipients' tables
- ll(4) && do_log(4, sprintf("check_for_banned (%s) %s",
- join(',', map {$_->base_name} @path), $key_val_trad_str));
- my($result,$matchingkey); my($t_ref_old);
- for my $e (@recip_tables) { # for each recipient and his tables
- my($found,$recip,$t_ref) = @$e{'found','recip','tables'};
- if (!$e->{result} && $t_ref && @$t_ref) {
- my($same_as_prev) = $t_ref_old && @$t_ref_old==@$t_ref &&
- !(grep { $t_ref_old->[$_] ne $t_ref->[$_] }
- (0..$#$t_ref)) ? 1 : 0;
- if ($same_as_prev) {
- do_log(4,"skip banned check for $recip, ".
- "same tables as previous, result => $result");
- } else {
- do_log(5,"doing banned check for $recip on ".$key_val_trad_str);
- ($result,$matchingkey) =
- lookup(0, [map {@$_} @descr_trad], # check all attribs in one go
- Amavis::Lookup::Label->new("check_bann:$recip"),
- map { ref($_) eq 'ARRAY' ? @$_ : $_ } @$t_ref);
- $t_ref_old = $t_ref;
- }
- @$e{'found','result','matchk','part_descr'} =
- (1,$result,$matchingkey,$key_val_trad_str) if defined $result;
- }
- }
- if (ref $bnpre && ref $$bnpre &&
- grep {!$_->{result}} @recip_tables) { # any non-true remains
- # try new style: banned_namepath_re; it is global, not per-recipient
- my($result,$matchingkey) = lookup(0, join("\n",@descr),
- Amavis::Lookup::Label->new('banned_namepath_re'), $bnpre);
- if (defined $result) {
- for my $e (@recip_tables) {
- @$e{'found','result','matchk','part_descr'} =
- (1,$result,$matchingkey,$key_val_str) if !$e->{found};
- }
- }
- }
- my(%esc) = (r => "\r", n => "\n", f => "\f", b => "\b",
- e => "\e", a => "\a", t => "\t"); # for pretty-printing
- my($ll) = (grep {$_->{result}} @recip_tables) ? 1 : 3; # log level
- for my $e (@recip_tables) { # log and store results
- my($r,$recip,$result,$matchingkey,$part_descr) =
- @$e{'r','recip','result','matchk','part_descr'};
- if (ll($ll)) { # only bother with logging when needed
- my($mk) = defined $matchingkey ? $matchingkey : ''; # pretty-print
- $mk =~ s{ \\(.) }{ exists($esc{$1}) ? $esc{$1} : '\\'.$1 }egsx;
- do_log($result?1:3, sprintf('p.path%s %s: "%s"%s',
- !$result?'':" BANNED:$result", $recip, $key_val_str,
- !defined $result ? '' : ", matching_key=\"$mk\""));
- }
- my($a);
- if ($result) { # the part being tested is banned for this recipient
- $a = $r->banned_parts; $a = [] if !defined($a);
- push(@$a,$part_descr); $r->banned_parts($a);
- $a = $r->banned_keys; $a = [] if !defined($a);
- push(@$a,$matchingkey); $r->banned_keys($a);
- $a = $r->banned_rhs; $a = [] if !defined($a);
- push(@$a,$result); $r->banned_rhs($a);
- }
- }
- last if !grep {!$_->{result}} @recip_tables; # stop if all recips true
- } # endfor: message tree traversal
- } # endif: doing parts checking
- }
- 1;
- #
- package Amavis::Unpackers::MIME;
- use strict;
- use re 'taint';
- BEGIN {
- use Exporter ();
- use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.043';
- @ISA = qw(Exporter);
- @EXPORT_OK = qw(&mime_decode);
- }
- use Errno qw(ENOENT EACCES);
- use IO::File qw(O_CREAT O_EXCL O_WRONLY);
- use MIME::Parser;
- use MIME::Words;
- BEGIN {
- import Amavis::Conf qw(:platform c cr ca);
- import Amavis::Timing qw(section_time);
- import Amavis::Util qw(snmp_count ll do_log);
- import Amavis::Unpackers::NewFilename qw(consumed_bytes);
- }
- use subs @EXPORT_OK;
- # save MIME preamble and epilogue (if nontrivial) as extra (pseudo)parts
- sub mime_decode_pre_epi($$$$$) {
- my($pe_name, $pe_lines, $tempdir, $parent_obj, $placement) = @_;
- if (defined $pe_lines && @$pe_lines) {
- do_log(5, "mime_decode_$pe_name: " . scalar(@$pe_lines) . " lines");
- if (@$pe_lines > 5 || "@$pe_lines" !~ m{^[a-zA-Z0-9/\@:;,. \t\n_-]*\z}s) {
- my($newpart_obj) =
- Amavis::Unpackers::Part->new("$tempdir/parts",$parent_obj,1);
- $newpart_obj->mime_placement($placement);
- $newpart_obj->name_declared($pe_name);
- my($newpart) = $newpart_obj->full_name;
- my($outpart) = IO::File->new;
- $outpart->open($newpart, O_CREAT|O_EXCL|O_WRONLY, 0640)
- or die "Can't create $pe_name file $newpart: $!";
- binmode($outpart, ":bytes") or die "Can't cancel :utf8 mode: $!"
- if $unicode_aware;
- my($len);
- for (@$pe_lines) {
- $outpart->print($_) or die "Can't write $pe_name to $newpart: $!";
- $len += length($_);
- }
- $outpart->close or die "Error closing $pe_name $newpart: $!";
- $newpart_obj->size($len);
- consumed_bytes($len, "mime_decode_$pe_name", 0, 1);
- }
- }
- }
- # traverse MIME::Entity object depth-first,
- # extracting preambles and epilogues as extra (pseudo)parts, and
- # filling-in additional information into Amavis::Unpackers::Part objects
- sub mime_traverse($$$$$); # prototype
- sub mime_traverse($$$$$) {
- my($entity, $tempdir, $parent_obj, $depth, $placement) = @_;
- mime_decode_pre_epi('preamble', $entity->preamble,
- $tempdir, $parent_obj, $placement);
- my($mt, $et) = ($entity->mime_type, $entity->effective_type);
- my($part); my($head) = $entity->head; my($body) = $entity->bodyhandle;
- if (!defined($body)) { # a MIME container only contains parts, no bodypart
- # create pseudo-part objects for MIME containers (e.g. multipart/* )
- $part = Amavis::Unpackers::Part->new(undef,$parent_obj,1);
- # $part->type_short('no-file');
- do_log(2, $part->base_name." $placement Content-Type: $mt");
- } else { # does have a body part (i.e. not a MIME container)
- my($fn) = $body->path; my($size);
- if (!defined($fn)) { $size = length($body->as_string) }
- else {
- my($msg); my($errn) = lstat($fn) ? 0 : 0+$!;
- if ($errn == ENOENT) { $msg = "does not exist" }
- elsif ($errn) { $msg = "is inaccessible: $!" }
- elsif (!-r _) { $msg = "is not readable" }
- elsif (!-f _) { $msg = "is not a regular file" }
- else {
- $size = -s _;
- do_log(4,"mime_traverse: file $fn is empty") if !$size;
- }
- do_log(-1,"WARN: mime_traverse: file $fn $msg") if defined $msg;
- }
- consumed_bytes($size, 'mime_decode', 0, 1);
- # retrieve Amavis::Unpackers::Part object (if any), stashed into head obj
- $part = Amavis::Unpackers::OurFiler::get_amavisd_part($head);
- if (defined $part) {
- $part->size($size);
- if ($size==0) { $part->type_short('empty'); $part->type_long('empty') }
- ll(2) && do_log(2, $part->base_name." $placement Content-Type: $mt" .
- ", size: $size B, name: ".$entity->head->recommended_filename);
- my($old_parent_obj) = $part->parent;
- if ($parent_obj ne $old_parent_obj) { # reparent if necessary
- ll(5) && do_log(5,sprintf("reparenting %s from %s to %s",
- $part->base_name,
- $old_parent_obj->base_name, $parent_obj->base_name));
- my($ch_ref) = $old_parent_obj->children;
- $old_parent_obj->children([grep {$_ ne $part} @$ch_ref]);
- $ch_ref = $parent_obj->children;
- push(@$ch_ref,$part); $parent_obj->children($ch_ref);
- $part->parent($parent_obj);
- }
- }
- }
- if (defined $part) {
- $part->mime_placement($placement);
- $part->type_declared($mt eq $et ? $mt : [$mt, $et]);
- my(@rn); # recommended file names, both raw and RFC 2047 decoded
- my($val, $val_decoded);
- $val = $head->mime_attr('content-disposition.filename');
- if ($val ne '') {
- push(@rn, $val);
- $val_decoded = MIME::Words::decode_mimewords($val);
- push(@rn, $val_decoded) if $val_decoded ne $val;
- }
- $val = $head->mime_attr('content-type.name');
- if (defined($val) && $val ne '') {
- $val_decoded = MIME::Words::decode_mimewords($val);
- push(@rn, $val_decoded) if !grep { $_ eq $val_decoded } @rn;
- push(@rn, $val) if !grep { $_ eq $val } @rn;
- }
- $part->name_declared(@rn==1 ? $rn[0] : \@rn) if @rn;
- }
- mime_decode_pre_epi('epilogue', $entity->epilogue,
- $tempdir, $parent_obj, $placement);
- my($item_num) = 0;
- for my $e ($entity->parts) { # recursive descent
- $item_num++;
- mime_traverse($e,$tempdir,$part,$depth+1,"$placement/$item_num");
- }
- }
- # Break up mime parts, return MIME::Entity object
- sub mime_decode($$$) {
- my($fileh, $tempdir, $parent_obj) = @_;
- # $fileh may be an open file handle, or a file name
- my($parser) = MIME::Parser->new;
- $parser->filer(Amavis::Unpackers::OurFiler->new("$tempdir/parts",
- $parent_obj));
- $parser->ignore_errors(1); # also is the default
- # $parser->extract_nested_messages(0);
- $parser->extract_nested_messages("NEST"); # parse embedded message/rfc822
- $parser->extract_uuencode(1); # to enable or not to enable ???
- my($entity);
- snmp_count('OpsDecByMimeParser');
- if (ref($fileh)) { # assume open file handle
- do_log(4, "Extracting mime components");
- $fileh->seek(0,0) or die "Can't rewind mail file: $!";
- local($1,$2,$3,$4); # avoid Perl 5.8.0 & 5.8.2 bug, $1 gets tainted !
- $entity = $parser->parse($fileh);
- } else { # assume $fileh is a file name
- do_log(4, "Extracting mime components from $fileh");
- local($1,$2,$3,$4); # avoid Perl 5.8.0 & 5.8.2 bug, $1 gets tainted !
- $entity = $parser->parse_open("$tempdir/parts/$fileh");
- }
- # my($mime_err) = $parser->last_error; # deprecated
- my($mime_err) = $parser->results->errors;
- if (defined $mime_err) {
- $mime_err=~s/\s+\z//; $mime_err=~s/[ \t\r]*\n+/; /g; $mime_err=~s/\s+/ /g;
- $mime_err = substr($mime_err,0,250) . '...' if length($mime_err) > 250;
- do_log(1, "WARN: MIME::Parser $mime_err") if $mime_err ne '';
- }
- mime_traverse($entity, $tempdir, $parent_obj, 0, '1');
- section_time('mime_decode');
- ($entity, $mime_err);
- }
- 1;
- #
- package Amavis::Notify;
- use strict;
- use re 'taint';
- BEGIN {
- use Exporter ();
- use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.043';
- @ISA = qw(Exporter);
- @EXPORT_OK = qw(&delivery_status_notification &delivery_short_report
- &string_to_mime_entity &defanged_mime_entity
- &msg_from_quarantine);
- }
- BEGIN {
- import Amavis::Util qw(ll do_log am_id safe_encode q_encode);
- import Amavis::Timing qw(section_time);
- import Amavis::Conf qw(:platform $myhostname c cr ca);
- import Amavis::Lookup qw(lookup);
- import Amavis::Expand qw(expand);
- import Amavis::rfc2821_2822_Tools;
- }
- use MIME::Entity;
- # use Encode; # Perl 5.8 UTF-8 support
- use subs @EXPORT_OK;
- # Convert mail (that was obtained by macro-expanding notification templates)
- # into proper MIME::Entity object. Some ad-hoc solutions are used
- # for compatibility with previous version.
- #
- sub string_to_mime_entity($) {
- my($mail_as_string_ref) = @_;
- local($1,$2,$3); my($entity); my($m_hdr,$m_body);
- ($m_hdr, $m_body) = ($1, $3)
- if $$mail_as_string_ref =~ /^(.*?\r?\n)(\r?\n|\z)(.*)\z/s;
- $m_body = safe_encode(c('bdy_encoding'), $m_body);
- # make sure _our_ source line number is reported in case of failure
- my($nxmh) = c('notify_xmailer_header');
- eval {$entity = MIME::Entity->build(
- Type => 'text/plain', Encoding => '-SUGGEST', Charset=> c('bdy_encoding'),
- (defined $nxmh && $nxmh eq '' ? () # leave the MIME::Entity default
- : ('X-Mailer' => $nxmh) ), # X-Mailer hdr or undef
- Data => $m_body); 1} or do {chomp($@); die $@};
- my($head) = $entity->head;
- # insert header fields from template into MIME::Head entity
- $m_hdr =~ s/\r?\n([ \t])/$1/g; # unfold template header
- for my $hdr_line (split(/\r?\n/, $m_hdr)) {
- if ($hdr_line =~ /^([^:]*):\s*(.*)\z/s) {
- my($fhead, $fbody) = ($1, $2);
- # encode according to RFC 2047 if necessary
- $fhead = safe_encode('ascii', $fhead);
- if ($fhead =~ /^(X-.*|Subject|Comments)\z/si &&
- $fbody =~ /[^\011\012\040-\176]/) # nonprint. except TAB and LF?
- { # encode according to RFC 2047
- # TODO: shouldn't we unfold first?!
- my($fbody_octets);
- if (!$unicode_aware) { $fbody_octets = $fbody }
- else {
- $fbody_octets = safe_encode(c('hdr_encoding'), $fbody);
- do_log(5, "string_to_mime_entity UTF-8 body: $fbody");
- do_log(5, "string_to_mime_entity body octets: $fbody_octets");
- }
- my($qb) = c('hdr_encoding_qb');
- if (uc($qb) eq 'Q') {
- $fbody = q_encode($fbody_octets, $qb, c('hdr_encoding'));
- } else {
- $fbody = MIME::Words::encode_mimeword($fbody_octets,
- $qb, c('hdr_encoding'));
- }
- } else { # supposed to be in plain ASCII, let's make sure it is
- $fbody = safe_encode('ascii', $fbody);
- }
- do_log(5, sprintf("string_to_mime_entity %s: %s", $fhead, $fbody));
- # make sure _our_ source line number is reported in case of failure
- if (!eval { $head->replace($fhead, $fbody); 1 }) {
- chomp($@);
- die sprintf("%s header field '%s: %s'",
- ($@ eq '' ? "invalid" : "$@, "), $fhead, $fbody);
- }
- }
- }
- $entity; # return the built MIME::Entity
- }
- # Generate delivery status notification according to
- # rfc1892 (now rfc3462) and rfc1894 (now rfc3464).
- # Return dsn message object if dsn is needed, or undef otherwise.
- #
- sub delivery_status_notification($$$$$) {
- my($conn,$msginfo,$report_success_dsn_also,$builtins_ref,$template_ref) = @_;
- my($dsn_time) = time; # time of dsn creation - now
- my($notification);
- if ($msginfo->sender eq '') { # must not respond to null reverse path
- do_log(4, "Not sending DSN to empty return path");
- } else {
- my($from_mta, $client_ip) = ($conn->smtp_helo, $conn->client_ip);
- my($msg) = ''; # constructed dsn text according to rfc3464
- $msg .= "Reporting-MTA: dns; $myhostname\n";
- $msg .= "Received-From-MTA: smtp; $from_mta ([$client_ip])\n"
- if $from_mta ne '';
- $msg .= "Arrival-Date: " . rfc2822_timestamp($msginfo->rx_time) . "\n";
- my($any); # any recipients with failed delivery?
- for my $r (@{$msginfo->per_recip_data}) {
- my($remote_mta) = $r->recip_remote_mta;
- my($smtp_resp) = $r->recip_smtp_response;
- if (!$r->recip_done) {
- if ($msginfo->delivery_method eq '') { # e.g. milter
- # as far as we are concerned all is ok, delivery will be performed
- # by a helper program or MTA
- $smtp_resp = "250 2.5.0 Ok, continue delivery";
- } else {
- do_log(-2,"TROUBLE: recipient not done: <"
- . $r->recip_addr . "> " . $smtp_resp);
- }
- }
- my($smtp_resp_code, $smtp_resp_enhcode, $smtp_resp_msg);
- if ($smtp_resp =~ /^ (\d{3}) \s+ ([245] \. \d{1,3} \. \d{1,3})?
- \s* (.*) \z/xs) {
- ($smtp_resp_code, $smtp_resp_enhcode, $smtp_resp_msg) = ($1,$2,$3);
- } else {
- $smtp_resp_msg = $smtp_resp;
- }
- my($smtp_resp_class) = $smtp_resp_code =~ /^(\d)/ ? $1 : '0';
- if ($smtp_resp_enhcode eq '' && $smtp_resp_class =~ /^([245])\z/) {
- $smtp_resp_enhcode = "$1.0.0";
- }
- # skip success notifications
- next unless $smtp_resp_class ne '2' || $report_success_dsn_also;
- $any++;
- $msg .= "\n"; # empty line between groups of per-recipient fields
- if ($remote_mta ne '' && $r->recip_final_addr ne $r->recip_addr) {
- $msg .= "X-NextToLast-Final-Recipient: rfc822; "
- . quote_rfc2821_local($r->recip_addr) . "\n";
- $msg .= "Final-Recipient: rfc822; "
- . quote_rfc2821_local($r->recip_final_addr) . "\n";
- } else {
- $msg .= "Final-Recipient: rfc822; "
- . quote_rfc2821_local($r->recip_addr) . "\n";
- }
- $msg .= "Action: ".($smtp_resp_class eq '2' ? 'delivered':'failed')."\n";
- $msg .= "Status: $smtp_resp_enhcode\n";
- my($rem_smtp_resp) = $r->recip_remote_mta_smtp_response;
- if ($remote_mta eq '' || $rem_smtp_resp eq '') {
- $msg .= "Diagnostic-Code: smtp; $smtp_resp\n";
- } else {
- $msg .= "Remote-MTA: dns; $remote_mta\n";
- $msg .= "Diagnostic-Code: smtp; $rem_smtp_resp\n";
- }
- $msg .= "Last-Attempt-Date: " . rfc2822_timestamp($dsn_time) . "\n";
- }
- return $notification if !$any; # don't bother, we won't be sending DSN
- my($to_hdr) = qquote_rfc2821_local($msginfo->sender_contact);
- # use the provided template text
- my(%mybuiltins) = %$builtins_ref; # make a local copy
- # not really needed, these header fields are overridden later
- $mybuiltins{'f'} = c('hdrfrom_notify_sender');
- $mybuiltins{'T'} = $to_hdr;
- $mybuiltins{'d'} = rfc2822_timestamp($dsn_time);
- my($dsn) = expand($template_ref, \%mybuiltins);
- my($dsn_entity) = string_to_mime_entity($dsn);
- $dsn_entity->make_multipart;
- my($head) = $dsn_entity->head;
- # rfc3464: The From field of the message header of the DSN SHOULD contain
- # the address of a human who is responsible for maintaining the mail system
- # at the Reporting MTA site (e.g. Postmaster), so that a reply to the
- # DSN will reach that person.
- # Override header fields from the template:
- eval { $head->replace('From', c('hdrfrom_notify_sender')); 1 }
- or do { chomp($@); die $@ };
- eval { $head->replace('To', $to_hdr); 1 } or do { chomp($@); die $@ };
- eval { $head->replace('Date', rfc2822_timestamp($dsn_time)); 1 }
- or do { chomp($@); die $@ };
- my($field) = Mail::Field->new('Content_type'); # underline, not hyphen!
- $field->type("multipart/report; report-type=delivery-status");
- $field->boundary(MIME::Entity::make_boundary());
- $head->replace('Content-type', $field->stringify);
- $head = undef;
- # make sure _our_ source line number is reported in case of failure
- eval {$dsn_entity->attach(
- Type => 'message/delivery-status', Encoding => '7bit',
- Description => 'Delivery error report',
- Data => $msg); 1} or do {chomp($@); die $@};
- eval {$dsn_entity->attach(
- Type => 'text/rfc822-headers', Encoding => '-SUGGEST',
- Description => 'Undelivered-message headers',
- Data => $msginfo->orig_header); 1} or do {chomp($@); die $@};
- $notification = Amavis::In::Message->new;
- $notification->rx_time($dsn_time);
- # $notification->body_type('7BIT');
- $notification->delivery_method(c('notify_method'));
- $notification->sender(c('mailfrom_notify_sender')); # should be empty!
- $notification->auth_submitter('<>');
- $notification->auth_user(c('amavis_auth_user'));
- $notification->auth_pass(c('amavis_auth_pass'));
- $notification->recips([$msginfo->sender_contact]);
- $notification->mail_text($dsn_entity);
- }
- $notification;
- }
- # Return a triple of arrayrefs of quoted recipient addresses (the first lists
- # recipients with successful delivery status, the second all the rest),
- # plus a list of short per-recipient delivery reports for failed deliveries,
- # that can be used in the first MIME part (the free text format) of delivery
- # status notifications.
- #
- sub delivery_short_report($) {
- my($msginfo) = @_;
- my(@succ_recips, @failed_recips, @failed_recips_full);
- for my $r (@{$msginfo->per_recip_data}) {
- my($remote_mta) = $r->recip_remote_mta;
- my($smtp_resp) = $r->recip_smtp_response;
- my($qrecip_addr) = scalar(qquote_rfc2821_local($r->recip_addr));
- if ($r->recip_destiny == D_PASS && ($smtp_resp=~/^2/ || !$r->recip_done)) {
- push(@succ_recips, $qrecip_addr);
- } else {
- push(@failed_recips, $qrecip_addr);
- push(@failed_recips_full, sprintf("%s:%s\n %s", $qrecip_addr,
- (!defined($remote_mta)||$remote_mta eq '' ? '' : " $remote_mta said:"),
- $smtp_resp));
- }
- }
- (\@succ_recips, \@failed_recips, \@failed_recips_full);
- }
- # Build a new MIME::Entity object based on the original mail, but hopefully
- # safer to mail readers: conventional mail header fields are retained,
- # original mail becomes an attachment of type 'message/rfc822'.
- # Text in $first_part becomes the first MIME part of type 'text/plain'.
- #
- sub defanged_mime_entity($$$) {
- my($conn,$msginfo,$first_part) = @_;
- my($new_entity);
- $first_part = safe_encode(c('bdy_encoding'), $first_part);
- # make sure _our_ source line number is reported in case of failure
- my($nxmh) = c('notify_xmailer_header');
- eval {$new_entity = MIME::Entity->build(
- Type => 'multipart/mixed',
- (defined $nxmh && $nxmh eq '' ? () # leave the MIME::Entity default
- : ('X-Mailer' => $nxmh) ), # X-Mailer hdr or undef
- ); 1} or do {chomp($@); die $@};
- my($head) = $new_entity->head;
- my($orig_head) = $msginfo->mime_entity->head;
- # TODO: we should retain the ordering of Resent-* with their Received fields
- for my $field_head ( # copy some of the original header fields
- qw(Received From Sender To Cc Reply-To Date Message-ID
- Resent-From Resent-Sender Resent-To Resent-Cc
- Resent-Date Resent-Message-ID
- In-Reply-To References Subject
- Comments Keywords Organization X-Mailer) ) {
- for my $value ($orig_head->get_all($field_head)) {
- do_log(4, "copying-over the header field: $field_head");
- eval { $head->add($field_head, $value); 1 } or do {chomp($@); die $@};
- }
- }
- $head = undef; # object not needed any longer
- eval {$new_entity->attach(
- Type => 'text/plain', Encoding => '-SUGGEST', Charset => c('bdy_encoding'),
- Data => $first_part); 1} or do {chomp($@); die $@};
- eval {$new_entity->attach( # rfc2046
- Type => 'message/rfc822; x-spam-type=original',
- Encoding => '8bit', Path => $msginfo->mail_text_fn,
- Description => 'Original message',
- Filename => 'message.txt', Disposition => 'attachment'); 1}
- or do {chomp($@); die $@};
- $new_entity;
- }
- # Fill-in message object information based on a quarantined mail
- sub msg_from_quarantine($$) {
- my($conn,$msginfo) = @_;
- my($fh) = $msginfo->mail_text;
- my($fname) = $msginfo->mail_text_fn;
- my($quarantine_id) = $msginfo->mail_id;
- $msginfo->delivery_method(c('notify_method')); # c('forward_method') ???
- $msginfo->auth_submitter('<>');
- $msginfo->auth_user(c('amavis_auth_user'));
- $msginfo->auth_pass(c('amavis_auth_pass'));
- $fh->seek(0,0) or die "Can't rewind mail file: $!";
- my($qid,$sender,@recips,$curr_head); my($ln); my($bsmtp) = 0;
- # extract envelope information from the quarantine file
- do_log(4, "msg_from_quarantine: releasing $quarantine_id");
- for (undef $!; defined($ln=$fh->getline); undef $!) {
- if ($ln =~ /^[ \t]/) { $curr_head .= $ln }
- else {
- my($next_head) = $ln; local($1,$2);
- local($_) = $curr_head; chomp; s/\n([ \t])/$1/g; # unfold
- if (!defined($curr_head)) { # first time
- } elsif (/^(EHLO|HELO)( |$)/i) { $bsmtp = 1;
- } elsif (/^MAIL FROM:\s*(<.*>)(.*)$/i) {
- $bsmtp = 1; $sender = $1; $sender = unquote_rfc2821_local($sender);
- } elsif ( $bsmtp && /^RCPT TO:\s*(<.*>)(.*)$/i) {
- push(@recips, unquote_rfc2821_local($1));
- } elsif ( $bsmtp && /^(DATA|NOOP)$/i) {
- } elsif ( $bsmtp && /^RSET$/i) { $sender = undef; @recips = ();
- } elsif (!$bsmtp && /^Return-Path:\s*(.*)$/i) {
- } elsif (!$bsmtp && /^Delivered-To:\s*(.*)$/i) {
- } elsif (!$bsmtp && /^X-Envelope-From:\s*<(.*)>$/i) {
- $sender = $1; $sender = unquote_rfc2821_local($sender);
- } elsif (!$bsmtp && /^X-Envelope-To:\s*(.*)$/i) {
- my($to) = $1;
- push(@recips, map {unquote_rfc2821_local($_)}
- ($to =~ /\G < ([^>]*) > (?: , \s* )?/gcx) );
- } elsif (/^X-Quarantine-Id:\s*(.*)$/i) {
- $qid = $1; $qid = $1 if $qid =~ /^<(.*)>\z/s;
- } else {
- last; # end of known headers
- }
- last if $next_head eq "\n"; # end-of-header reached
- $curr_head = $next_head;
- }
- }
- defined $ln || $!==0 or die "Error reading file $fname: $!";
- do_log(1,sprintf("Quarantined message: %s %s -> %s", $qid,
- qquote_rfc2821_local($sender),
- join(',', qquote_rfc2821_local(@recips)) ));
- my(@m);
- push(@m,'missing X-Quarantine-Id') if !defined $qid;
- push(@m,'missing '.($bsmtp?'MAIL FROM':'X-Envelope-From')) if !defined $sender;
- push(@m,'missing '.($bsmtp?'RCPT TO' :'X-Envelope-To')) if !@recips;
- if (!defined($msginfo->sender)) { $msginfo->sender($sender) }
- else { # sender specified in the request, overrides stored info
- push(@m, sprintf("overriding sender %s by %s",
- qquote_rfc2821_local($sender, $msginfo->sender) ));
- }
- if (!defined($msginfo->per_recip_data)) { $msginfo->recips(\@recips) }
- else { # recipients specified in the request, overrides stored info
- push(@m, sprintf("overriding recips %s by %s",
- join(',', qquote_rfc2821_local(@recips)),
- join(',', qquote_rfc2821_local(@{$msginfo->recips})) ));
- }
- do_log(0, "Quarantine release $quarantine_id: ".join("; ",@m)) if @m;
- my($hdr_edits) = Amavis::Out::EditHeader->new;
- for my $h (qw(Return-Path Delivered-To X-Quarantine-Id
- X-Envelope-From X-Envelope-To X-Amavis-Hold))
- { $hdr_edits->delete_header($h) }
- $hdr_edits->prepend_header('Received',
- received_line($conn,$msginfo,am_id(),1), 1);
- # prepend Resent-* header fields, they must precede
- # corresponding Received header field (pushed in reverse order)
- # "Resent-From:" and "Resent-Date:" are required fields!
- $hdr_edits->prepend_header('Resent-Message-ID',
- sprintf('<QR%s@%s>',$msginfo->mail_id,$myhostname) );
- $hdr_edits->prepend_header('Resent-Date', # time of the release request
- rfc2822_timestamp($msginfo->rx_time));
- $hdr_edits->prepend_header('Resent-To',
- @{$msginfo->recips} != 1 ? 'undisclosed-recipients:;'
- : qquote_rfc2821_local(@{$msginfo->recips}));
- if ($msginfo->requested_by eq '') {
- $hdr_edits->prepend_header('Resent-From', c('hdrfrom_notify_recip'));
- } else {
- $hdr_edits->prepend_header('Resent-Sender', c('hdrfrom_notify_recip'));
- $hdr_edits->prepend_header('Resent-From',
- qquote_rfc2821_local($msginfo->requested_by));
- }
- $msginfo->header_edits($hdr_edits);
- if ($qid ne $quarantine_id)
- { die "Stored quarantine ID '$qid' does not match ".
- "requested ID '$quarantine_id'" }
- if ($bsmtp)
- { die "Releasing messages in BSMTP format not yet supported ".
- "(dot stuffing not implemented)" }
- $msginfo;
- }
- 1;
- #
- package Amavis::Cache;
- # offer an 'IPC::Cache'-compatible simple interface
- # to a local (per-process) memory-based cache;
- use strict;
- use re 'taint';
- BEGIN {
- import Amavis::Util qw(ll do_log);
- }
- BEGIN {
- use Exporter ();
- use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.0431';
- @ISA = qw(Exporter);
- }
- # simple local memory-based cache
- sub new { # called by each child process
- my($class) = @_;
- do_log(5,"BerkeleyDB-based Amavis::Cache not available, ".
- "using memory-based local cache");
- bless {}, $class;
- }
- sub get { my($self,$key) = @_; thaw($self->{$key}) }
- sub set { my($self,$key,$obj) = @_; $self->{$key} = freeze($obj) }
- # protect % and ~, as well as NUL and \200 for good measure
- sub encode($) {
- my($str) = @_; $str =~ s/[%~\000\200]/sprintf("%%%02X",ord($&))/egs; $str;
- }
- # simple Storable::freeze lookalike
- sub freeze($); # prototype
- sub freeze($) {
- my($obj) = @_; my($ty) = ref($obj);
- if (!defined($obj)) { 'U' }
- elsif (!$ty) { join('~', '', encode($obj)) } # string
- elsif ($ty eq 'SCALAR') { join('~', 'S', encode(freeze($$obj))) }
- elsif ($ty eq 'REF') { join('~', 'R', encode(freeze($$obj))) }
- elsif ($ty eq 'ARRAY') { join('~', 'A', map {encode(freeze($_))} @$obj) }
- elsif ($ty eq 'HASH') {
- join('~','H',map {(encode($_),encode(freeze($obj->{$_})))} sort keys %$obj)
- } else { die "Can't freeze object type $ty" }
- }
- # simple Storable::thaw lookalike
- sub thaw($); # prototype
- sub thaw($) {
- my($str) = @_;
- return undef if !defined $str;
- my($ty,@val) = split(/~/,$str,-1);
- for (@val) { s/%([0-9a-fA-F]{2})/pack("C",hex($1))/eg }
- if ($ty eq 'U') { undef }
- elsif ($ty eq '') { $val[0] }
- elsif ($ty eq 'S') { my($obj)=thaw($val[0]); \$obj }
- elsif ($ty eq 'R') { my($obj)=thaw($val[0]); \$obj }
- elsif ($ty eq 'A') { [map {thaw($_)} @val] }
- elsif ($ty eq 'H') {
- my($hr) = {};
- while (@val) { my($k) = shift @val; $hr->{$k} = thaw(shift @val) }
- $hr;
- } else { die "Can't thaw object type $ty" }
- }
- 1;
- #
- package Amavis;
- require 5.005; # need qr operator and \z in regexps
- use strict;
- use re 'taint';
- use Errno qw(ENOENT EACCES);
- use POSIX qw(locale_h);
- use IO::File ();
- use Time::HiRes ();
- # body digest for caching, either SHA1 or MD5
- #use Digest::SHA1;
- use Digest::MD5;
- use Net::Server 0.83;
- use Net::Server::PreForkSimple;
- BEGIN {
- import Amavis::Conf qw(:platform :sa :confvars c cr ca);
- import Amavis::Util qw(untaint min max ll do_log sanitize_str debug_oneshot
- am_id add_entropy generate_mail_id
- snmp_counters_init snmp_count prolong_timer);
- import Amavis::Log qw(open_log close_log);
- import Amavis::Timing qw(section_time get_time_so_far);
- import Amavis::rfc2821_2822_Tools;
- import Amavis::Lookup qw(lookup);
- import Amavis::Lookup::IP qw(lookup_ip_acl);
- import Amavis::Out;
- import Amavis::Out::EditHeader;
- import Amavis::UnmangleSender qw(best_try_originator_ip best_try_originator
- first_received_from);
- import Amavis::Unpackers::Validity qw(
- check_header_validity check_for_banned_names);
- import Amavis::Unpackers::MIME qw(mime_decode);
- import Amavis::Expand qw(expand);
- import Amavis::Notify qw(delivery_status_notification delivery_short_report
- string_to_mime_entity defanged_mime_entity);
- import Amavis::In::Connection;
- import Amavis::In::Message;
- }
- # Make it a subclass of Net::Server::PreForkSimple
- # to override method &process_request (and others if desired)
- use vars qw(@ISA);
- # @ISA = qw(Net::Server);
- @ISA = qw(Net::Server::PreForkSimple);
- add_entropy(Time::HiRes::gettimeofday, $$, $], @INC, %ENV);
- delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};
- use vars qw(
- $extra_code_db $extra_code_cache
- $extra_code_sql_base $extra_code_sql_log $extra_code_sql_quar
- $extra_code_sql_lookup $extra_code_ldap
- $extra_code_in_amcl $extra_code_in_smtp
- $extra_code_antivirus $extra_code_antispam $extra_code_unpackers);
- use vars qw(%modules_basic);
- use vars qw($spam_level $spam_status $spam_report);
- use vars qw($user_id_sql $wb_listed_sql $implicit_maps_inserted);
- use vars qw($db_env $snmp_db);
- use vars qw($body_digest_cache);
- use vars qw(%builtins); # customizable notification messages
- use vars qw($child_invocation_count $child_task_count);
- # $child_invocation_count # counts child re-use from 1 to max_requests
- # $child_task_count # counts check_mail_begin_task (and check_mail) calls;
- # this often runs in sync with $child_invocation_count,
- # but with SMTP or LMTP input there may be more than one
- # message passed during a single SMTP session
- use vars qw(@config_files);
- use vars qw($CONN $MSGINFO);
- use vars qw($av_output @virusname @detecting_scanners
- $banned_filename_any $banned_filename_all @bad_headers);
- use vars qw($amcl_in_obj $smtp_in_obj); # Amavis::In::AMCL and In::SMTP objects
- use vars qw($sql_dataset_conn_lookups); # Amavis::Out::SQL::Connection object
- use vars qw($sql_dataset_conn_storage); # Amavis::Out::SQL::Connection object
- use vars qw($sql_storage); # Amavis::Out::SQL::Log object
- use vars qw($sql_policy $sql_wblist); # Amavis::Lookup::SQL objects
- use vars qw($ldap_connection); # Amavis::LDAP::Connection object
- use vars qw($ldap_policy); # Amavis::Lookup::LDAP object
- # initialize the %builtins, which is an associative array of built-in macros
- # to be used in notification message expansion.
- sub init_builtin_macros() {
- # A key (macro name) must be a single character. Most characters are
- # allowed, but to be on the safe side and for clarity it is suggested
- # that only letters are used. Upper case letters may (as a mnemonic)
- # suggest the value is an array, lower case may suggest the value is
- # a scalar string - but this is only a convention and not enforced.
- #
- # A value may be a reference to a subroutine which will be called later at
- # the time of macro expansion. This way we can provide a method for obtaining
- # information which is not yet available at the time of initialization, such
- # as AV scanner results, or provide a lazy evaluation for more expensive
- # calculations. Subroutine will be called in scalar context with no args.
- # It may return a scalar string (or undef), or an array reference.
- #
- %builtins = (
- '.' => undef,
- p => sub {c('policy_bank_path')},
- # mail reception timestamp (e.g. start of a SMTP transaction):
- d => sub {rfc2822_timestamp($MSGINFO->rx_time)}, # rfc2822 local date-time
- # U => sub {iso8601_timestamp($MSGINFO->rx_time)}, # iso8601, local time
- U => sub {iso8601_utc_timestamp($MSGINFO->rx_time)}, # iso8601 UTC
- y => sub {sprintf("%.0f", 1000*get_time_so_far())}, # elapsed time in ms
- u => sub {sprintf("%010d",$MSGINFO->rx_time)}, # s since Unix epoch (UTC)
- h => $myhostname, # dns name of this host, or configurable name
- l => sub {my($ip) = $MSGINFO->client_addr; my($val);
- $val = $ip ne '' ? lookup_ip_acl($ip,@{ca('mynetworks_maps')})
- : lookup(0,$MSGINFO->sender_source,
- @{ca('local_domains_maps')});
- $val ? 1 : undef}, # sender's client IP (if known) from @mynetworks
- # (if IP is known), or sender domain is local
- s => sub {qquote_rfc2821_local($MSGINFO->sender)}, # original envelope sender in <>
- S => sub { # unmangled sender or sender address to be notified, or empty...
- sanitize_str($MSGINFO->sender_contact) }, # ..if sender unknown
- o => sub { # best attempt at determining true sender (origin) of the virus,
- sanitize_str($MSGINFO->sender_source) }, # normally same as %s
- R => sub {$MSGINFO->recips}, # original message recipients list
- D => sub {my($y,$n,$f)=delivery_short_report($MSGINFO); $y}, # succ.delivered
- O => sub {my($y,$n,$f)=delivery_short_report($MSGINFO); $n}, # failed recips
- N => sub {my($y,$n,$f)=delivery_short_report($MSGINFO); $f}, # short dsn
- Q => sub {$MSGINFO->queue_id}, # MTA queue ID of the message if known
- m => sub { local($_) = $MSGINFO->mime_entity; # Message-ID of the message
- if (defined) { $_ = $_->head->get('Message-ID',0);
- if (defined) {
- chomp; s/^[ \t]+//; s/[ \t\n]+\z//; # trim
- # protect space and \n, other special chars...
- # ...will be sanitized before logging
- s{([ =\r\n])}{sprintf("=%02X",ord($1))}eg;
- }; $_ }},
- r => sub { local($_) = $MSGINFO->mime_entity; # first Resent-Message-ID
- if (defined) { $_ = $_->head->get('Resent-Message-ID',0);
- if (defined) {
- chomp; s/^[ \t]+//; s/[ \t\n]+\z//; # trim
- s{([ =\r\n])}{sprintf("=%02X",ord($1))}eg;
- }; $_ }},
- j => sub { local($_) = $MSGINFO->mime_entity; # Subject of the message
- if (defined) { $_ = $_->head->get('Subject',0); chomp;
- s/\n([ \t])/$1/g; # unfold
- s{([=\r\n])}{sprintf("=%02X",ord($1))}eg; $_ }},
- b => sub {$MSGINFO->body_digest}, # original message body digest
- n => \&am_id, # amavis internal message id (for log entries)
- i => sub {$MSGINFO->mail_id}, # long-term unique mail id on this system
- q => sub {my($q) = $MSGINFO->quarantined_to;
- !defined($q) ? undef :
- [map { my($m)=$_; $m=~s{^\Q$QUARANTINEDIR\E/}{}; $m } @$q];
- }, # list of quarantine mailboxes
- v => sub {[split(/[ \t]*\r?\n/,$av_output)]}, # anti-virus scanner output
- V => sub {my(%seen); [grep {!$seen{$_}++} @virusname]}, #unique virus names
- F => sub { my(%seen); # list of banned file names
- my(@b) = grep { !$seen{$_}++ }
- map { @{$_->banned_parts} }
- grep { defined $_->banned_parts }
- @{$MSGINFO->per_recip_data};
- my($b_chopped) = @b > 2; @b = (@b[0,1],'...') if $b_chopped;
- s/[ \t]{6,}/ ... /g for @b;
- \@b },
- X => sub {\@bad_headers}, # list of header syntax violations
- W => sub {\@detecting_scanners}, # list of av scanners detecting a virus
- H => sub {[map {my $h=$_; chomp($h); $h} @{$MSGINFO->orig_header}]},# orig hdr
- A => sub {[split(/\r?\n/, $spam_report)]}, # SpamAssassin report lines
- c => sub { if (!defined($spam_level)) { '-' }
- else { # format SA score +/- by-sender score boosts
- my($sl) = 0+sprintf("%.3f",$spam_level); # trim down fraction
- my(@boost) = map { my($b) = $_->recip_score_boost;
- !defined($b) ? undef : 0+sprintf("%.3f",$b)
- } @{$MSGINFO->per_recip_data};
- !(grep { defined($_) && $_ != 0 } @boost) ? $sl
- : @boost==1 ? ($boost[0]>=0 ?$sl.'+'.$boost[0] :$sl.$boost[0])
- : $sl . '+(' . join(',',@boost) . ')';
- }
- },
- z => sub {$MSGINFO->msg_size}, # mail size
- t => sub { # first entry in the Received trace
- sanitize_str(first_received_from($MSGINFO->mime_entity)) },
- e => sub { # first valid public IP in the Received trace
- sanitize_str(best_try_originator_ip($MSGINFO->mime_entity)) },
- a => sub {$MSGINFO->client_addr}, # original SMTP session client IP address
- g => sub { # original SMTP session client DNS name
- sanitize_str($MSGINFO->client_name) },
- k => sub { my($kill_level);
- scalar(grep # any recipient declared the message be killed ?
- { !$_->recip_whitelisted_sender &&
- ($_->recip_blacklisted_sender ||
- ($kill_level=lookup(0,$_->recip_addr,
- @{ca('spam_kill_level_maps')}),
- defined $spam_level && defined $kill_level &&
- $spam_level + $_->recip_score_boost >= $kill_level) )
- } @{$MSGINFO->per_recip_data}) },
- '1'=> sub { my($tag_level);
- scalar(grep # above tag level for any recipient?
- { !$_->recip_whitelisted_sender &&
- ($_->recip_blacklisted_sender ||
- ($tag_level=lookup(0,$_->recip_addr,
- @{ca('spam_tag_level_maps')}),
- defined $spam_level && defined $tag_level &&
- $spam_level + $_->recip_score_boost >= $tag_level) )
- } @{$MSGINFO->per_recip_data}) },
- '2'=> sub { my($tag2_level);
- scalar(grep # above tag2 level for any recipient?
- { !$_->recip_whitelisted_sender &&
- ($_->recip_blacklisted_sender ||
- ($tag2_level=lookup(0,$_->recip_addr,
- @{ca('spam_tag2_level_maps')}),
- defined $spam_level && defined $tag2_level &&
- $spam_level + $_->recip_score_boost >= $tag2_level) )
- } @{$MSGINFO->per_recip_data}) },
- # macros f, T, C, B will be defined for each notification as appropriate
- # (representing From:, To:, Cc:, and Bcc: respectively)
- # remaining free letters: wxyEGIJKLMPYZ
- );
- }
- # initialize %local_delivery_aliases
- sub init_local_delivery_aliases() {
- # The %local_delivery_aliases maps local virtual 'localpart' to a mailbox
- # (e.g. to a quarantine filename or a directory). Used by method 'local:',
- # i.e. in mail_to_local_mailbox(), for direct local quarantining.
- # The hash value may be a ref to a pair of fixed strings, or a subroutine ref
- # (which must return a pair of strings (a list, not a list ref)) which makes
- # possible lazy evaluation when some part of the pair is not known before
- # the final delivery time. The first string in a pair must be either:
- # - empty or undef, which will disable saving the message,
- # - a filename, indicating a Unix-style mailbox,
- # - a directory name, indicating a maildir-style mailbox,
- # in which case the second string may provide a suggested file name.
- #
- %Amavis::Conf::local_delivery_aliases = (
- 'virus-quarantine' => sub { ($QUARANTINEDIR, undef) },
- 'banned-quarantine' => sub { ($QUARANTINEDIR, undef) },
- 'bad-header-quarantine' => sub { ($QUARANTINEDIR, undef) },
- 'spam-quarantine' => sub { ($QUARANTINEDIR, undef) },
- # some more examples:
- 'archive-files' => sub { ("$QUARANTINEDIR", undef) },
- 'archive-mbox' => sub { ("$QUARANTINEDIR/archive.mbox", undef) },
- 'recip-quarantine' => sub { ("$QUARANTINEDIR/recip-archive.mbox",undef) },
- 'sender-quarantine' =>
- sub { my($s) = $MSGINFO->sender;
- $s = substr($s,0,100)."..." if length($s) > 100+3;
- $s =~ tr/a-zA-Z0-9@._+-]/=/c; $s =~ s/\@/_at_/g;
- $s = untaint($s) if $s =~ /^(?:[a-zA-Z0-9%=._+-]+)\z/; # untaint
- ($QUARANTINEDIR, "sender-$s-%m.gz"); # suggested file name
- },
- # 'recip-quarantine2' => sub {
- # my(@fnames);
- # my($myfield) =
- # Amavis::Lookup::SQLfield->new($sql_policy,'some_field_name','S');
- # for my $r (@{$MSGINFO->recips}) {
- # my($field_value) = lookup(0,$r,$myfield);
- # my($fname) = $field_value; # or perhaps: my($fname) = $r;
- # local($1); $fname =~ s/[^a-zA-Z0-9._@]/=/g; $fname =~ s/\@/%/g;
- # $fname = untaint($fname) if $fname =~ /^([a-zA-Z0-9._=%]+)\z/;
- # $fname =~ s/%/%%/g; # protect %
- # do_log(3, "Recipient: $r, field: $field_value, fname: $fname");
- # push(@fnames, $fname);
- # }
- # # ???what file name to choose if there is more than one recipient???
- # ( $QUARANTINEDIR, "sender-$fnames[0]-%i-%n.gz" ); # suggested file name
- # },
- );
- }
- # initialize some remaining global variables;
- # invoked after chroot and after privileges have been dropped
- sub after_chroot_init() {
- $child_invocation_count = $child_task_count = 0;
- %modules_basic = %INC; # helps to track missing modules in chroot
- my(@msg);
- my($euid) = $>; # effective UID
- $> = 0; # try to become root
- POSIX::setuid(0) if $> != 0; # and try some more
- if ($> == 0) { # succeded? panic!
- @msg = ("It is possible to change EUID from $euid to root, ABORTING!",
- "Please use the most recent Net::Server or apply a patch - see:",
- " http://www.ijs.si/software/amavisd/#net-server-sec",
- "or start as non-root, e.g. by su(1) or using option -u user");
- } elsif ($daemon_chroot_dir eq '') {
- # A quick check on vulnerability/protection of a config file
- # (non-exhaustive: doesn't test for symlink tricks and higher directories).
- # The config file has already been executed by now, so it may be
- # too late to feel sorry now, but better late then never.
- for my $config_file (@config_files) {
- my($fh) = IO::File->new;
- my($errn) = lstat($config_file) ? 0 : 0+$!;
- if ($errn) { # not accessible, don't bother to test further
- } elsif ($fh->open($config_file,'+<')) {
- push(@msg, "Config file \"$config_file\" is writable, ".
- "UID $<, EUID $>, EGID $)" );
- $fh->close; # close, ignoring status
- } elsif (rename($config_file, $config_file.'.moved')) {
- my($m) = 'appears writable (unconfirmed)';
- if (!-e($config_file) && -e($config_file.'.moved')) {
- rename($config_file.'.moved', $config_file); # try to rename back
- $m = 'is writable (confirmed)';
- }
- push(@msg, "Directory of a config file \"$config_file\" $m, ".
- "UID $<, EUID $>, EGID $)" );
- }
- last if @msg;
- }
- }
- if (@msg) {
- do_log(-3,"FATAL: $_") for @msg;
- print STDERR (map {"$_\n"} @msg);
- die "SECURITY PROBLEM, ABORTING";
- exit 1; # just in case
- }
- # report versions of some modules
- for my $m ('Amavis::Conf',
- sort map { s/\.pm\z//; s[/][::]g; $_ } grep { /\.pm\z/ } keys %INC){
- next if !grep { $_ eq $m } qw(Amavis::Conf
- Archive::Tar Archive::Zip Compress::Zlib Convert::TNEF Convert::UUlib
- MIME::Entity MIME::Parser MIME::Tools Mail::Header Mail::Internet
- Mail::ClamAV Mail::SpamAssassin Mail::SpamAssassin::SpamCopURI URI
- Razor2::Client::Version Mail::SPF::Query Authen::SASL
- IO::Socket::INET6 Net::DNS Net::SMTP Net::Cmd Net::Server Net::LDAP
- DBI DBD::mysql DBD::SQLite BerkeleyDB DB_File
- SAVI Unix::Syslog Time::HiRes);
- do_log(0, sprintf("Module %-19s %s", $m, $m->VERSION || '?'));
- }
- if (c('forward_method') eq '' && $extra_code_in_smtp) {
- do_log(1,"forward_method in default policy bank is null (milter setup?), ".
- "DISABLING SMTP-in AS A PRECAUTION");
- $extra_code_in_smtp = undef;
- }
- do_log(0,"Amavis::DB code ".($extra_code_db ?'':" NOT")." loaded");
- do_log(0,"Amavis::Cache code".($extra_code_cache ?'':" NOT")." loaded");
- do_log(0,"SQL base code ".($extra_code_sql_base ?'':" NOT")." loaded");
- do_log(0,"SQL::Log code ".($extra_code_sql_log ?'':" NOT")." loaded");
- do_log(0,"SQL::Quarantine ".($extra_code_sql_quar ?'':" NOT")." loaded");
- do_log(0,"Lookup::SQL code ".($extra_code_sql_lookup?'':" NOT")." loaded");
- do_log(0,"Lookup::LDAP code ".($extra_code_ldap ?'':" NOT")." loaded");
- do_log(0,"AM.PDP prot code ".($extra_code_in_amcl ?'':" NOT")." loaded");
- do_log(0,"SMTP-in prot code ".($extra_code_in_smtp ?'':" NOT")." loaded");
- do_log(0,"ANTI-VIRUS code ".($extra_code_antivirus?'':" NOT")." loaded");
- do_log(0,"ANTI-SPAM code ".($extra_code_antispam ?'':" NOT")." loaded");
- do_log(0,"Unpackers code ".($extra_code_unpackers?'':" NOT")." loaded");
- # store policy names into 'policy_bank_name' fields, if not explicitly set
- for my $name (keys %policy_bank) {
- if (ref($policy_bank{$name}) eq 'HASH' &&
- !exists($policy_bank{$name}{'policy_bank_name'})) {
- $policy_bank{$name}{'policy_bank_name'} = $name;
- $policy_bank{$name}{'policy_bank_path'} = $name;
- }
- }
- };
- # overlay the current policy bank by settings from the
- # $policy_bank{$policy_bank_name}, or load the default policy bank (empty name)
- sub load_policy_bank($) {
- my($policy_bank_name) = @_;
- if (!exists $policy_bank{$policy_bank_name}) {
- do_log(-1,"policy bank \"$policy_bank_name\" does not exist, ignored");
- } elsif ($policy_bank_name eq '') {
- %current_policy_bank = %{$policy_bank{$policy_bank_name}};
- do_log(4,'loaded base policy bank');
- } else {
- my($cpbp) = c('policy_bank_path'); # currently loaded bank
- for my $k (keys %{$policy_bank{$policy_bank_name}}) {
- do_log(-1,"loading policy bank \"$policy_bank_name\": ".
- "unknown field \"$k\"") if !exists $current_policy_bank{$k};
- $current_policy_bank{$k} = $policy_bank{$policy_bank_name}{$k};
- }
- $current_policy_bank{'policy_bank_path'} =
- ($cpbp eq '' ? '' : $cpbp.'/') . $policy_bank_name;
- do_log(2,sprintf('loaded policy bank "%s"%s', $policy_bank_name,
- $cpbp eq '' ? '' : " over \"$cpbp\""));
- }
- }
- ### Net::Server hook
- ### This hook occurs in the parent (master) process after chroot,
- ### change of user, and change of group has occured. It allows
- ### for preparation before looping begins.
- sub pre_loop_hook {
- my($self) = @_;
- local $SIG{CHLD} = 'DEFAULT';
- eval {
- after_chroot_init(); # the rest of the top-level initialization
- # this needs to be done only after chroot, otherwise paths will be wrong
- find_external_programs([split(/:/,$path,-1)]); # path, decoders, scanners
- # do some sanity checking
- my($name) = $TEMPBASE;
- $name = "$daemon_chroot_dir $name" if $daemon_chroot_dir ne '';
- my($errn) = stat($TEMPBASE) ? 0 : 0+$!;
- if ($errn==ENOENT) { die "No TEMPBASE directory: $name" }
- elsif ($errn) { die "TEMPBASE directory inaccessible, $!: $name" }
- elsif (!-d _) { die "TEMPBASE is not a directory: $name" }
- elsif (!-w _) { die "TEMPBASE directory is not writable: $name" }
- if ($enable_global_cache && $extra_code_db) {
- my($name) = $db_home;
- $name = "$daemon_chroot_dir $name" if $daemon_chroot_dir ne '';
- $errn = stat($db_home) ? 0 : 0+$!;
- if ($errn == ENOENT) {
- die "Please create an empty directory $name to hold a database".
- " (config variable \$db_home)\n" }
- elsif ($errn) { die "db_home inaccessible, $!: $name" }
- elsif (!-d _) { die "db_home is not a directory : $name" }
- elsif (!-w _) { die "db_home directory is not writable: $name" }
- Amavis::DB::init(1);
- }
- if ($QUARANTINEDIR ne '') {
- my($name) = $QUARANTINEDIR;
- $name = "$daemon_chroot_dir $name" if $daemon_chroot_dir ne '';
- $errn = stat($QUARANTINEDIR) ? 0 : 0+$!;
- if ($errn == ENOENT) { } # ok
- elsif ($errn) { die "QUARANTINEDIR inaccessible, $!: $name" }
- elsif (-d _ && !-w _) { die "QUARANTINEDIR directory not writable: $name" }
- }
- Amavis::SpamControl::init() if $extra_code_antispam;
- };
- if ($@ ne '') {
- chomp($@); my($msg) = "TROUBLE in pre_loop_hook: $@"; do_log(-2,$msg);
- die ("Suicide (" . am_id() . ") " . $msg . "\n");
- }
- 1;
- }
- ### log routine Net::Server hook
- ### (Sys::Syslog MUST NOT be specified as a value of 'log_file'!)
- #
- # Redirect Net::Server logging to use Amavis' do_log().
- # The main reason is that Net::Server uses Sys::Syslog
- # (and has two bugs in doing it, at least the Net-Server-0.82),
- # and Amavis users are acustomed to Unix::Syslog.
- sub write_to_log_hook {
- my($self,$level,$msg) = @_;
- my($prop) = $self->{server};
- local $SIG{CHLD} = 'DEFAULT';
- chomp($msg);
- do_log(1, "Net::Server: " . $msg); # just call Amavis' traditional logging
- 1;
- }
- ### user customizable Net::Server hook (Net::Server 0.88 or later),
- ### hook occurs in the master process
- sub run_n_children_hook {
- Amavis::AV::sophos_savi_reload()
- if $extra_code_antivirus && Amavis::AV::sophos_savi_stale();
- add_entropy(Time::HiRes::gettimeofday);
- }
- ### compatibility with patched Net::Server by SAVI patch (Net::Server <= 0.87)
- sub parent_fork_hook { my($self) = @_; $self->run_n_children_hook }
- ### user customizable Net::Server hook
- sub child_init_hook {
- my($self) = @_;
- local $SIG{CHLD} = 'DEFAULT';
- $0 = 'amavisd (virgin child)';
- my($inherited_entropy);
- eval {
- $db_env = $snmp_db = $body_digest_cache = undef; # just in case
- Amavis::Timing::init(); snmp_counters_init();
- close_log(); open_log(); # reopen syslog or log file to get per-process fd
- if ($extra_code_db) {
- $db_env = Amavis::DB->new; # get access to a bdb environment
- $snmp_db = Amavis::DB::SNMP->new($db_env);
- $snmp_db->register_proc('') if defined $snmp_db; # process alive & idle
- my($var_ref) = $snmp_db->read_snmp_variables('entropy');
- $inherited_entropy = $var_ref->[0] if $var_ref && @$var_ref;
- }
- # if $db_env is undef the Amavis::Cache::new creates a memory-based cache
- $body_digest_cache = Amavis::Cache->new($db_env);
- if ($extra_code_db) { # is it worth reporting the timing? (probably not)
- section_time('bdb-open');
- do_log(2, Amavis::Timing::report()); # report elapsed times
- }
- # Prepare permanent SQL dataset connection objects, does not connect yet!
- # $sql_dataset_conn_lookups and $sql_dataset_conn_storage may be the
- # same dataset (one connection used), or they may be separate objects,
- # which will make separate connections to distinct datasets,
- # possibly using different SQL engine types or servers
- if ($extra_code_sql_lookup && @lookup_sql_dsn) {
- $sql_dataset_conn_lookups =
- Amavis::Out::SQL::Connection->new(@lookup_sql_dsn);
- }
- if ($extra_code_sql_log && @storage_sql_dsn) {
- if (!$sql_dataset_conn_lookups || @storage_sql_dsn != @lookup_sql_dsn
- || grep { $storage_sql_dsn[$_] ne $lookup_sql_dsn[$_] }
- (0..$#storage_sql_dsn) )
- { # DSN differs or no SQL lookups, storage needs its own connection
- $sql_dataset_conn_storage =
- Amavis::Out::SQL::Connection->new(@storage_sql_dsn);
- do_log(2,"storage and lookups will use separate connections to SQL")
- if $sql_dataset_conn_lookups;
- } else { # same dataset, use the same database connection object
- $sql_dataset_conn_storage = $sql_dataset_conn_lookups;
- do_log(2,"storage and lookups will use the same connection to SQL");
- }
- }
- # Make storage/lookup objs to hold DBI handles and 'prepared' statements.
- $sql_storage = Amavis::Out::SQL::Log->new($sql_dataset_conn_storage)
- if $sql_dataset_conn_storage;
- $sql_policy = Amavis::Lookup::SQL->new($sql_dataset_conn_lookups,
- 'sel_policy') if $sql_dataset_conn_lookups;
- $sql_wblist = Amavis::Lookup::SQL->new($sql_dataset_conn_lookups,
- 'sel_wblist') if $sql_dataset_conn_lookups;
- };
- if ($@ ne '') {
- chomp($@); do_log(-2, "TROUBLE in child_init_hook: $@");
- die "Suicide in child_init_hook: $@\n";
- }
- add_entropy($$, Time::HiRes::gettimeofday, $inherited_entropy);
- Amavis::Timing::go_idle('vir');
- }
- ### user customizable Net::Server hook
- sub post_accept_hook {
- my($self) = @_;
- local $SIG{CHLD} = 'DEFAULT';
- $child_invocation_count++;
- $0 = sprintf("amavisd (ch%d-accept)", $child_invocation_count);
- Amavis::Timing::go_busy('hi ');
- # establish initial time right after 'accept'
- Amavis::Timing::init(); snmp_counters_init();
- $snmp_db->register_proc('A') if defined $snmp_db; # in 'accept' state
- load_policy_bank(''); # start with a builting policy bank
- }
- ### user customizable Net::Server hook
- ### if this hook returns 1 the request is processed
- ### if this hook returns 0 the request is denied
- sub allow_deny_hook {
- my($self) = @_;
- local($1,$2,$3,$4); # Perl bug: $1 and $2 come tainted from Net::Server !
- local $SIG{CHLD} = 'DEFAULT';
- my($prop) = $self->{server}; my($sock) = $prop->{client}; my($bank_name);
- my($is_ux) = UNIVERSAL::can($sock,'NS_proto') && $sock->NS_proto eq 'UNIX';
- if ($is_ux) {
- $bank_name = $interface_policy{"SOCK"}; # possibly undef
- } else {
- my($myif,$myport) = ($prop->{sockaddr}, $prop->{sockport});
- if (defined $interface_policy{"$myif:$myport"}) {
- $bank_name = $interface_policy{"$myif:$myport"};
- } elsif (defined $interface_policy{$myport}) {
- $bank_name = $interface_policy{$myport};
- }
- }
- load_policy_bank($bank_name) if defined $bank_name &&
- $bank_name ne c('policy_bank_name');
- # note that the new policy bank may have replaced the inet_acl access table
- if ($is_ux) {
- # always permit access - unix sockets are immune to this check
- } else {
- my($permit,$fullkey,$err) = lookup_ip_acl($prop->{peeraddr},
- Amavis::Lookup::Label->new('inet_acl'), ca('inet_acl'));
- if (defined($err) && $err ne '') {
- do_log(-1, sprintf("DENIED ACCESS due to INVALID IP ADDRESS %s: %s",
- $prop->{peeraddr}, $err));
- return 0;
- } elsif (!$permit) {
- my($msg) = sprintf("DENIED ACCESS from IP %s, policy bank '%s'",
- $prop->{peeraddr}, c('policy_bank_name') );
- $msg .= ", blocked by rule $fullkey" if defined $fullkey;
- do_log(-1,$msg);
- return 0;
- }
- }
- 1;
- }
- # use Fcntl qw(F_GETFD F_SETFD FD_CLOEXEC);
- # sub cloexec_on($;$) {
- # my($fd,$name) = @_; my($flags);
- # $flags = fcntl($fd, F_GETFD, 0)
- # or die "Can't get flags from the file descriptor: $!";
- # if ($flags & FD_CLOEXEC == 0) {
- # do_log(4,"Turning on FD_CLOEXEC flag on $name");
- # fcntl($fd, F_SETFD, $flags | FD_CLOEXEC)
- # or die "Can't set FD_CLOEXEC on file descriptor $name: $!";
- # }
- # }
- ### The heart of the program
- ### user customizable Net::Server hook
- sub process_request {
- my($self) = shift;
- my($prop) = $self->{server}; my($sock) = $prop->{client};
- local $SIG{CHLD} = 'DEFAULT';
- local($1,$2,$3,$4); # Perl bug: $1 and $2 come tainted from Net::Server !
- # Net::Server assigns STDIN and STDOUT to the socket
- binmode(STDIN) or die "Can't set STDIN to binmode: $!";
- binmode(STDOUT) or die "Can't set STDOUT to binmode: $!";
- binmode($sock) or die "Can't set socket to binmode: $!";
- $| = 1;
- local $SIG{ALRM} = sub { die "timed out\n" }; # do not modify the sig text!
- eval {
- # if ($] < 5.006) { # Perl older than 5.6.0 did not set FD_CLOEXEC on sockets
- # for my $mysock (@{$prop->{sock}}) { cloexec_on($mysock, $mysock) }
- # }
- prolong_timer('new request - timer reset', $child_timeout); # timer init
- if ($extra_code_ldap && !defined $ldap_policy) {
- # make LDAP lookup object
- $ldap_connection = Amavis::LDAP::Connection->new($default_ldap);
- $ldap_policy = Amavis::Lookup::LDAP->new($default_ldap,$ldap_connection)
- if $ldap_connection;
- }
- if (defined $ldap_policy && !$implicit_maps_inserted) {
- # make LDAP field lookup objects with incorporated field names
- # fieldtype: B=boolean, N=numeric, S=string, L=list
- # B-, N-, S-, L- returns undef if field does not exist
- # B0: boolean, nonexistent field treated as false,
- # B1: boolean, nonexistent field treated as true
- my $lf = sub{Amavis::Lookup::LDAPattr->new($ldap_policy,@_)};
- unshift(@Amavis::Conf::virus_lovers_maps, $lf->('amavisVirusLover', 'B-'));
- unshift(@Amavis::Conf::spam_lovers_maps, $lf->('amavisSpamLover', 'B-'));
- unshift(@Amavis::Conf::banned_files_lovers_maps, $lf->('amavisBannedFilesLover', 'B-'));
- unshift(@Amavis::Conf::bad_header_lovers_maps, $lf->('amavisBadHeaderLover', 'B-'));
- unshift(@Amavis::Conf::bypass_virus_checks_maps, $lf->('amavisBypassVirusChecks', 'B-'));
- unshift(@Amavis::Conf::bypass_spam_checks_maps, $lf->('amavisBypassSpamChecks', 'B-'));
- unshift(@Amavis::Conf::bypass_banned_checks_maps,$lf->('amavisBypassBannedChecks', 'B-'));
- unshift(@Amavis::Conf::bypass_header_checks_maps,$lf->('amavisBypassHeaderChecks', 'B-'));
- unshift(@Amavis::Conf::spam_tag_level_maps, $lf->('amavisSpamTagLevel', 'N-'));
- unshift(@Amavis::Conf::spam_tag2_level_maps, $lf->('amavisSpamTag2Level', 'N-'));
- unshift(@Amavis::Conf::spam_kill_level_maps, $lf->('amavisSpamKillLevel', 'N-'));
- unshift(@Amavis::Conf::spam_modifies_subj_maps, $lf->('amavisSpamModifiesSubj', 'B-'));
- unshift(@Amavis::Conf::message_size_limit_maps, $lf->('amavisMessageSizeLimit', 'N-'));
- unshift(@Amavis::Conf::virus_quarantine_to_maps, $lf->('amavisVirusQuarantineTo', 'S-'));
- unshift(@Amavis::Conf::spam_quarantine_to_maps, $lf->('amavisSpamQuarantineTo', 'S-'));
- unshift(@Amavis::Conf::banned_quarantine_to_maps, $lf->('amavisBannedQuarantineTo','S-'));
- unshift(@Amavis::Conf::bad_header_quarantine_to_maps, $lf->('amavisBadHeaderQuarantineTo', 'S-'));
- unshift(@Amavis::Conf::local_domains_maps, $lf->('amavisLocal', 'B1'));
- unshift(@Amavis::Conf::warnvirusrecip_maps, $lf->('amavisWarnVirusRecip', 'B-'));
- unshift(@Amavis::Conf::warnbannedrecip_maps, $lf->('amavisWarnBannedRecip', 'B-'));
- unshift(@Amavis::Conf::warnbadhrecip_maps, $lf->('amavisWarnBadHeaderRecip', 'B-'));
- unshift(@Amavis::Conf::virus_admin_maps, $lf->('amavisVirusAdmin', 'S-'));
- unshift(@Amavis::Conf::newvirus_admin_maps, $lf->('amavisNewVirusAdmin', 'S-'));
- unshift(@Amavis::Conf::spam_admin_maps, $lf->('amavisSpamAdmin', 'S-'));
- unshift(@Amavis::Conf::banned_admin_maps, $lf->('amavisBannedAdmin', 'S-'));
- unshift(@Amavis::Conf::bad_header_admin_maps, $lf->('amavisBadHeaderAdmin', 'S-'));
- unshift(@Amavis::Conf::banned_filename_maps, $lf->('amavisBannedRuleNames', 'L-'));
- section_time('ldap-prepare');
- }
- if (defined $sql_policy && !$implicit_maps_inserted) {
- # make SQL field lookup objects with incorporated field names
- # fieldtype: B=boolean, N=numeric, S=string,
- # B-, N-, S- returns undef if field does not exist
- # B0: boolean, nonexistent field treated as false,
- # B1: boolean, nonexistent field treated as true
- my $nf = sub{Amavis::Lookup::SQLfield->new($sql_policy,@_)}; #shorthand
- $user_id_sql = $nf->('id', 'S');
- unshift(@Amavis::Conf::local_domains_maps, $nf->('local', 'B1'));
- unshift(@Amavis::Conf::virus_lovers_maps, $nf->('virus_lover', 'B-'));
- unshift(@Amavis::Conf::spam_lovers_maps, $nf->('spam_lover', 'B-'));
- unshift(@Amavis::Conf::banned_files_lovers_maps, $nf->('banned_files_lover', 'B-'));
- unshift(@Amavis::Conf::bad_header_lovers_maps, $nf->('bad_header_lover', 'B-'));
- unshift(@Amavis::Conf::bypass_virus_checks_maps, $nf->('bypass_virus_checks', 'B-'));
- unshift(@Amavis::Conf::bypass_spam_checks_maps, $nf->('bypass_spam_checks', 'B-'));
- unshift(@Amavis::Conf::bypass_banned_checks_maps, $nf->('bypass_banned_checks', 'B-'));
- unshift(@Amavis::Conf::bypass_header_checks_maps, $nf->('bypass_header_checks', 'B-'));
- unshift(@Amavis::Conf::spam_tag_level_maps, $nf->('spam_tag_level', 'N-'));
- unshift(@Amavis::Conf::spam_tag2_level_maps, $nf->('spam_tag2_level', 'N-'));
- unshift(@Amavis::Conf::spam_kill_level_maps, $nf->('spam_kill_level', 'N-'));
- unshift(@Amavis::Conf::spam_dsn_cutoff_level_maps,$nf->('spam_dsn_cutoff_level','N-'));
- unshift(@Amavis::Conf::spam_quarantine_cutoff_level_maps,$nf->('spam_quarantine_cutoff_level','N-'));
- unshift(@Amavis::Conf::spam_modifies_subj_maps, $nf->('spam_modifies_subj', 'B-'));
- unshift(@Amavis::Conf::spam_subject_tag_maps, $nf->('spam_subject_tag', 'S-'));
- unshift(@Amavis::Conf::spam_subject_tag2_maps, $nf->('spam_subject_tag2', 'S-'));
- unshift(@Amavis::Conf::virus_quarantine_to_maps, $nf->('virus_quarantine_to', 'S-'));
- unshift(@Amavis::Conf::banned_quarantine_to_maps, $nf->('banned_quarantine_to', 'S-'));
- unshift(@Amavis::Conf::bad_header_quarantine_to_maps, $nf->('bad_header_quarantine_to','S-'));
- unshift(@Amavis::Conf::spam_quarantine_to_maps, $nf->('spam_quarantine_to', 'S-'));
- unshift(@Amavis::Conf::message_size_limit_maps, $nf->('message_size_limit', 'N-'));
- unshift(@Amavis::Conf::addr_extension_virus_maps, $nf->('addr_extension_virus', 'S-'));
- unshift(@Amavis::Conf::addr_extension_spam_maps, $nf->('addr_extension_spam', 'S-'));
- unshift(@Amavis::Conf::addr_extension_banned_maps,$nf->('addr_extension_banned','S-'));
- unshift(@Amavis::Conf::addr_extension_bad_header_maps,$nf->('addr_extension_bad_header','S-'));
- unshift(@Amavis::Conf::warnvirusrecip_maps, $nf->('warnvirusrecip', 'B-'));
- unshift(@Amavis::Conf::warnbannedrecip_maps, $nf->('warnbannedrecip', 'B-'));
- unshift(@Amavis::Conf::warnbadhrecip_maps, $nf->('warnbadhrecip', 'B-'));
- unshift(@Amavis::Conf::newvirus_admin_maps, $nf->('newvirus_admin', 'S-'));
- unshift(@Amavis::Conf::virus_admin_maps, $nf->('virus_admin', 'S-'));
- unshift(@Amavis::Conf::banned_admin_maps, $nf->('banned_admin', 'S-'));
- unshift(@Amavis::Conf::bad_header_admin_maps, $nf->('bad_header_admin', 'S-'));
- unshift(@Amavis::Conf::spam_admin_maps, $nf->('spam_admin', 'S-'));
- unshift(@Amavis::Conf::banned_filename_maps, $nf->('banned_rulenames', 'S-'));
- section_time('sql-prepare');
- }
- Amavis::Conf::label_default_maps() if !$implicit_maps_inserted;
- $implicit_maps_inserted = 1;
- my($conn) = Amavis::In::Connection->new;
- $CONN = $conn; # ugly - save in a global
- $conn->proto($sock->NS_proto);
- my($suggested_protocol) = c('protocol'); # suggested by the policy bank
- ll(5) && do_log(5,"process_request: ".
- "suggested_protocol=\"$suggested_protocol\" on ".$sock->NS_proto);
- if ($sock->NS_proto eq 'UNIX') { # traditional amavis helper program
- if ($suggested_protocol eq 'COURIER') {
- die "unavailable support for protocol: $suggested_protocol";
- } elsif ($suggested_protocol eq 'AM.PDP') {
- $amcl_in_obj = Amavis::In::AMCL->new if !$amcl_in_obj;
- $amcl_in_obj->process_policy_request($sock, $conn, \&check_mail, 0);
- } else { # default to old amavis helper program protocol
- $amcl_in_obj = Amavis::In::AMCL->new if !$amcl_in_obj;
- $amcl_in_obj->process_policy_request($sock, $conn, \&check_mail, 1);
- }
- } elsif ($sock->NS_proto eq 'TCP') {
- $conn->socket_ip($prop->{sockaddr});
- $conn->socket_port($prop->{sockport});
- $conn->client_ip($prop->{peeraddr});
- if ($suggested_protocol eq 'TCP-LOOKUP') { # postfix maps (experimental)
- process_tcp_lookup_request($sock, $conn);
- do_log(2, Amavis::Timing::report()); # report elapsed times
- } elsif ($suggested_protocol eq 'AM.PDP') {
- # amavis policy delegation protocol (e.g. new milter helper program)
- $amcl_in_obj = Amavis::In::AMCL->new if !$amcl_in_obj;
- $amcl_in_obj->process_policy_request($sock, $conn, \&check_mail, 0);
- } else { # defaults to SMTP or LMTP
- if (!$extra_code_in_smtp) {
- die "incoming TCP connection, but dynamic SMTP/LMTP code not loaded";
- }
- $smtp_in_obj = Amavis::In::SMTP->new if !$smtp_in_obj;
- $smtp_in_obj->process_smtp_request(
- $sock, ($suggested_protocol eq 'LMTP'?1:0), $conn, \&check_mail);
- }
- } else {
- die ("unsupported protocol: $suggested_protocol, " . $sock->NS_proto);
- }
- }; # eval
- alarm(0); # stop the timer
- if ($@ ne '') {
- chomp($@); my($timed_out) = $@ eq "timed out";
- my($msg) = $timed_out ? "Child task exceeded $child_timeout seconds, abort"
- : "TROUBLE in process_request: $@";
- do_log(-2, $msg);
- $smtp_in_obj->preserve_evidence(1) if $smtp_in_obj && !$timed_out;
- # kills a child, hopefully preserving tempdir; does not kill parent
- do_log(-1, "Requesting process rundown after fatal error");
- $self->done(1);
- # die ("Suicide (" . am_id() . ") " . $msg . "\n");
- } elsif ($child_task_count >= $max_requests) {
- # in case of multiple-transaction protocols (e.g. SMTP, LMTP)
- # we do not like to keep running indefinitely at the mercy of MTA
- do_log(2, "Requesting process rundown after $child_task_count tasks ".
- "(and $child_invocation_count sessions)");
- $self->done(1);
- } elsif ($extra_code_antivirus && Amavis::AV::sophos_savi_stale() ) {
- do_log(0, "Requesting process rundown due to stale Sophos virus data");
- $self->done(1);
- }
- my(@modules_extra) = grep {!exists $modules_basic{$_}} keys %INC;
- # do_log(0, "modules loaded: ".join(", ", sort keys %modules_basic));
- do_log(1, "extra modules loaded: ".
- join(", ", sort @modules_extra)) if @modules_extra;
- }
- ### override Net::Server::PreForkSimple::done (needed for Net::Server <= 0.87)
- ### to be able to rundown the child process prematurely
- sub done(@) {
- my($self) = shift;
- if (@_) { $self->{server}->{done} = shift }
- elsif (!$self->{server}->{done})
- { $self->{server}->{done} = $self->SUPER::done }
- $self->{server}->{done};
- }
- ### Net::Server hook
- sub post_process_request_hook {
- my($self) = @_;
- local $SIG{CHLD} = 'DEFAULT';
- debug_oneshot(0);
- $0 = sprintf("amavisd (ch%d-avail)", $child_invocation_count);
- alarm(0); do_log(5,"post_process_request_hook: timer stopped");
- $snmp_db->register_proc('') if defined $snmp_db; # process is alive and idle
- Amavis::Timing::go_idle('bye'); Amavis::Timing::report_load();
- }
- ### Child is about to be terminated
- ### user customizable Net::Server hook
- sub child_finish_hook {
- my($self) = @_;
- local $SIG{CHLD} = 'DEFAULT';
- # for my $m (sort map { s/\.pm\z//; s[/][::]g; $_ } grep { /\.pm\z/ } keys %INC){
- # do_log(0, sprintf("Module %-19s %s", $m, $m->VERSION || '?'))
- # if grep {$m=~/^$_/} qw(Mail::ClamAV Mail::SpamAssassin Razor2 Net::DNS);
- # }
- $0 = sprintf("amavisd (ch%d-finish)", $child_invocation_count);
- do_log(5,"child_finish_hook: invoking DESTROY methods");
- $smtp_in_obj = undef; # calls Amavis::In::SMTP::DESTROY
- $amcl_in_obj = undef; # (currently does nothing for Amavis::In::AMCL)
- $sql_storage = undef; # calls Amavis::Out::SQL::Log::DESTROY
- $sql_wblist = undef; # calls Amavis::Lookup::SQL::DESTROY
- $sql_policy = undef; # calls Amavis::Lookup::SQL::DESTROY
- $ldap_policy = undef; # calls Amavis::Lookup::LDAP::DESTROY
- # calls Amavis::Out::SQL::Connection::DESTROY
- $sql_dataset_conn_lookups = $sql_dataset_conn_storage = undef;
- # calls Amavis::LDAP::Connection::DESTROY
- $ldap_connection = undef;
- $body_digest_cache = undef; # calls Amavis::Cache::DESTROY
- eval { $snmp_db->register_proc(undef) } if defined $snmp_db; # going away
- $snmp_db = undef; # calls Amavis::DB::SNMP::DESTROY
- $db_env = undef;
- }
- sub END { # runs before exiting the module
- # do_log(5,"at the END handler: invoking DESTROY methods");
- $smtp_in_obj = undef; # at end calls Amavis::In::SMTP::DESTROY
- $amcl_in_obj = undef; # (currently does nothing for Amavis::In::AMCL)
- $sql_storage = undef; # at end calls Amavis::Out::SQL::Log::DESTROY
- $sql_wblist = undef; # at end calls Amavis::Lookup::SQL::DESTROY
- $sql_policy = undef; # at end calls Amavis::Lookup::SQL::DESTROY
- $ldap_policy = undef; # at end calls Amavis::Lookup::LDAP::DESTROY
- # at end calls Amavis::Out::SQL::Connection::DESTROY
- $sql_dataset_conn_lookups = $sql_dataset_conn_storage = undef;
- # at end calls Amavis::LDAP::Connection::DESTROY
- $ldap_connection = undef;
- $body_digest_cache = undef; # at end calls Amavis::Cache::DESTROY
- eval { $snmp_db->register_proc(undef) } if defined $snmp_db; # going away
- $snmp_db = undef; # at end calls Amavis::DB::SNMP::DESTROY
- $db_env = undef;
- }
- # implements Postfix TCP lookup server, see tcp_table(5) man page; experimental
- sub process_tcp_lookup_request($$) {
- my($sock, $conn) = @_;
- local($/) = "\012"; # set line terminator to LF (regardless of platform)
- my($req_cnt); my($ln);
- for (undef $!; defined($ln=$sock->getline); undef $!) {
- $req_cnt++; my($level) = 0;
- my($resp_code, $resp_msg) = (400, 'INTERNAL ERROR');
- if ($ln =~ /^get (.*?)\015?\012\z/si) {
- my($key) = tcp_lookup_decode($1);
- my($sl); $sl = lookup(0,$key, @{ca('spam_lovers_maps')});
- $resp_code = 200; $level = 2;
- $resp_msg = $sl ? "OK Recipient <$key> IS spam lover"
- : "DUNNO Recipient <$key> is NOT spam lover";
- } elsif ($ln =~ /^put ([^ ]*) (.*?)\015?\012\z/si) {
- $resp_code = 500; $resp_msg = 'request not implemented: ' . $ln;
- } else {
- $resp_code = 500; $resp_msg = 'illegal request: ' . $ln;
- }
- do_log($level, "tcp_lookup($req_cnt): $resp_code $resp_msg");
- $sock->printf("%03d %s\012", $resp_code, tcp_lookup_encode($resp_msg))
- or die "Can't write to tcp_lookup socket: $!";
- }
- defined $ln || $!==0 or die "Error reading from socket: $!";
- do_log(0, "tcp_lookup: RUNDOWN after $req_cnt requests");
- }
- sub tcp_lookup_encode($) {
- my($str) = @_;
- $str =~ s/[^\041-\044\046-\176]/sprintf("%%%02x",ord($&))/eg;
- $str;
- }
- sub tcp_lookup_decode($) {
- my($str) = @_;
- $str =~ s/%([0-9a-fA-F]{2})/pack("C",hex($1))/eg;
- $str;
- }
- sub check_mail_begin_task() {
- # The check_mail_begin_task (and check_mail) may be called several times
- # per child lifetime and/or per-SMTP session. The variable $child_task_count
- # is mainly used by AV-scanner interfaces, e.g. to initialize when invoked
- # for the first time during child process lifetime
- $child_task_count++;
- do_log(4, "check_mail_begin_task: task_count=$child_task_count");
- # comment out to retain SQL/LDAP cache entries for the whole child lifetime:
- $sql_policy->clear_cache if defined $sql_policy;
- $sql_wblist->clear_cache if defined $sql_wblist;
- $ldap_policy->clear_cache if defined $ldap_policy;
- # reset certain global variables for each task
- $av_output = undef; @detecting_scanners = ();
- @virusname = (); @bad_headers = ();
- $banned_filename_any = $banned_filename_all = 0;
- $spam_level = undef; $spam_status = undef; $spam_report = undef;
- $MSGINFO = undef; # just in case
- }
- # Checks the message stored on a file. File must already
- # be open on file handle $msginfo->mail_text; it need not be positioned
- # properly, check_mail must not close the file handle.
- #
- sub check_mail($$$) {
- my($conn, $msginfo, $dsn_per_recip_capable) = @_;
- my($point_of_no_return) = 0; # past the point where mail or DSN was sent
- my($am_id) = am_id();
- $snmp_db->register_proc($am_id) if defined $snmp_db;
- my($tempdir) = $msginfo->mail_tempdir;
- my($fh) = $msginfo->mail_text; my(@recips) = @{$msginfo->recips};
- $MSGINFO = $msginfo; # ugly - save in a global, to make it accessible
- # to %builtins
- # compute body digest, measure mail size and check for 8-bit data
- my($body_digest) = get_body_digest($fh, $msginfo);
- my($mail_size) = $msginfo->msg_size; # use corrected ESMTP size if available
- if ($mail_size <= 0) { # not available?
- $mail_size = $msginfo->orig_header_size + 1 + $msginfo->orig_body_size;
- $msginfo->msg_size($mail_size); # store back
- }
- my($file_generator_object) = # maxfiles 0 disables the $MAXFILES limit
- Amavis::Unpackers::NewFilename->new($MAXFILES?$MAXFILES:undef, $mail_size);
- Amavis::Unpackers::Part::init($file_generator_object); # fudge: keep in variable
- my($parts_root) = Amavis::Unpackers::Part->new;
- $msginfo->parts_root($parts_root);
- my($smtp_resp, $exit_code, $preserve_evidence); my($virus_dejavu) = 0;
- my($virus_presence_checked,$spam_presence_checked);
- my($autolearn_status);
- # matching banned rules suggest DSN to be suppressed?
- my($banned_dsn_suppress) = 0;
- # is any mail component password protected or otherwise non-decodable?
- my($any_undecipherable) = 0;
- my($mime_err); # undef, or MIME parsing error string as given by MIME::Parser
- my($hold); # set to some string to cause the message to be placed on hold
- # (frozen) by MTA. This can be used in cases when we stumble
- # across some permanent problem making us unable to decide
- # if the message is to be really delivered.
- my($cl_ip) = $msginfo->client_addr;
- add_entropy(Time::HiRes::gettimeofday,
- "$child_task_count $am_id $cl_ip $mail_size", $msginfo->queue_id,
- $msginfo->mail_text_fn, $msginfo->sender, $msginfo->recips);
- my($mail_id);
- my($which_section);
- $which_section = 'gen_mail_id';
- # create unique mail_id and save preliminary information to SQL (if enabled)
- for (my($attempt)=5; $attempt>0; ) { # sanity limit on retries
- my($secret_id);
- ($mail_id,$secret_id) = generate_mail_id();
- $msginfo->secret_id($secret_id); $secret_id = '';
- $msginfo->mail_id($mail_id); # assign some long-term unique id to the msg
- if (!$sql_storage) { last } # no need to store and to check for uniqueness
- else { # attempt to save message placeholder to SQL ensuring it is unique
- $which_section = 'sql-enter';
- $sql_storage->save_info_preliminary($conn,$msginfo) and last;
- if (--$attempt <= 0) {
- do_log(-2,"ERROR sql_storage: too many retries ".
- "on storing preliminary, info not saved");
- } else {
- do_log(2,"sql_storage: retrying prelim., $attempt attempts remain");
- sleep(int(1+rand(3))); add_entropy(Time::HiRes::gettimeofday,$attempt);
- }
- }
- };
- section_time($which_section);
- my($pbn) = c('policy_bank_path');
- do_log(1,sprintf("Checking: %s %s%s%s -> %s", $mail_id,
- $pbn eq '' ? '' : "$pbn ",
- $cl_ip eq '' ? '' : "[$cl_ip] ",
- qquote_rfc2821_local($msginfo->sender),
- join(',', qquote_rfc2821_local(@recips)) ));
- eval {
- snmp_count('InMsgs');
- snmp_count('InMsgsNullRPath') if $msginfo->sender eq '';
- if (@recips == 1) { snmp_count( 'InMsgsRecips' ) }
- elsif (@recips > 1) { snmp_count( ['InMsgsRecips',scalar(@recips)] ) }
- # mkdir is a costly operation (must be atomic, flushes buffers).
- # If we can re-use directory 'parts' from the previous invocation it saves
- # us precious time. Together with matching rmdir this can amount to 10-15 %
- # of total elapsed time! (no spam checking, depending on file system)
- $which_section = "creating_partsdir";
- my($errn) = lstat("$tempdir/parts") ? 0 : 0+$!;
- if ($errn == ENOENT) { # needs to be created
- mkdir("$tempdir/parts", 0750)
- or die "Can't create directory $tempdir/parts: $!";
- section_time('mkdir parts'); }
- elsif ($errn != 0) { die "$tempdir/parts is not accessible: $!" }
- elsif (!-d _) { die "$tempdir/parts is not a directory" }
- else {} # fine, directory already exists
- chdir($TEMPBASE) or die "Can't chdir to $TEMPBASE: $!";
- # FIRST: what kind of e-mail did we get? call content scanners
- # already in cache?
- $which_section = "cached";
- snmp_count('CacheAttempts');
- my($cache_entry); my($now) = time;
- my($cache_entry_ttl) =
- max($virus_check_negative_ttl, $virus_check_positive_ttl,
- $spam_check_negative_ttl, $spam_check_positive_ttl);
- my($now_utc_iso8601) = iso8601_utc_timestamp($now,1);
- my($expires_utc_iso8601) = iso8601_utc_timestamp($now+$cache_entry_ttl,1);
- $cache_entry = $body_digest_cache->get($body_digest)
- if $body_digest_cache && defined $body_digest;
- if (!defined $cache_entry) {
- snmp_count('CacheMisses');
- $cache_entry->{'ctime'} = $now_utc_iso8601; # create a new cache record
- } else {
- snmp_count('CacheHits');
- $virus_presence_checked = defined $cache_entry->{'VN'} ? 1 : 0;
- # spam level and spam report may be influenced by mail header, not only
- # by mail body, so caching based on body is only a close approximation;
- # ignore spam cache if body is too small
- $spam_presence_checked = defined $cache_entry->{'SL'} ? 1 : 0;
- if ($msginfo->orig_body_size < 200) { $spam_presence_checked = 0 }
- if ($virus_presence_checked && defined $cache_entry->{'Vt'}) {
- # check for expiration of cached virus test results
- my($ttl) = !@{$cache_entry->{'VN'}} ? $virus_check_negative_ttl
- : $virus_check_positive_ttl;
- if ($now > $cache_entry->{'Vt'} + $ttl) {
- do_log(2,"Cached virus check expired, TTL = $ttl s");
- $virus_presence_checked = 0;
- }
- }
- if ($spam_presence_checked && defined $cache_entry->{'St'}) {
- # check for expiration of cached spam test results
- # (note: hard-wired spam level 6)
- my($ttl) = $cache_entry->{'SL'} < 6 ? $spam_check_negative_ttl
- : $spam_check_positive_ttl;
- if ($now > $cache_entry->{'St'} + $ttl) {
- do_log(2,"Cached spam check expired, TTL = $ttl s");
- $spam_presence_checked = 0;
- }
- }
- if ($virus_presence_checked) {
- $av_output = $cache_entry->{'VO'};
- @virusname = @{$cache_entry->{'VN'}};
- @detecting_scanners = @{$cache_entry->{'VD'}};
- $virus_dejavu = 1;
- }
- ($spam_level, $spam_status, $spam_report) = @$cache_entry{'SL','SS','SR'}
- if $spam_presence_checked;
- do_log(1,sprintf("cached %s from <%s> (%s,%s)",
- $body_digest, $msginfo->sender,
- $virus_presence_checked, $spam_presence_checked));
- snmp_count('CacheHitsVirusCheck') if $virus_presence_checked;
- snmp_count('CacheHitsVirusMsgs') if @virusname;
- snmp_count('CacheHitsSpamCheck') if $spam_presence_checked;
- snmp_count('CacheHitsSpamMsgs') if $spam_level >= 6; # a hack
- ll(5) && do_log(5,sprintf("cache entry age: %s c=%s a=%s",
- (@virusname ? 'V' : $spam_level > 5 ? 'S' : '.'),
- $cache_entry->{'ctime'}, $cache_entry->{'atime'} ));
- } # if defined $cache_entry
- my($will_do_virus_scanning, $all_bypass_virus_checks);
- if ($extra_code_antivirus) {
- $all_bypass_virus_checks =
- !grep {!lookup(0,$_, @{ca('bypass_virus_checks_maps')})} @recips;
- $will_do_virus_scanning =
- !$virus_presence_checked && !$all_bypass_virus_checks;
- }
- my($will_do_banned_checking) = # banned name checking will be needed?
- @{ca('banned_filename_maps')} || cr('banned_namepath_re');
- # will do decoding parts as deeply as possible? only if needed
- my($will_do_parts_decoding) =
- !c('bypass_decode_parts') &&
- ($will_do_virus_scanning || $will_do_banned_checking);
- $which_section = "mime_decode-1";
- my($ent); ($ent,$mime_err) = mime_decode($fh, $tempdir, $parts_root);
- $msginfo->mime_entity($ent);
- prolong_timer($which_section);
- if ($will_do_parts_decoding) { # decoding parts can take a lot of time
- $which_section = "parts_decode_ext";
- snmp_count('OpsDec');
- ($hold,$any_undecipherable) =
- Amavis::Unpackers::decompose_mail($tempdir,$file_generator_object);
- }
- if (grep {!lookup(0,$_,@{ca('bypass_header_checks_maps')})} @recips) {
- push(@bad_headers, "MIME error: ".$mime_err)
- if defined $mime_err && $mime_err ne '';
- push(@bad_headers, check_header_validity($conn,$msginfo));
- }
- if ($will_do_banned_checking) { # check for banned file contents
- $which_section = "check-banned";
- check_for_banned_names($msginfo,$parts_root); # saves results in $msginfo
- $banned_filename_any = 0; $banned_filename_all = 1;
- for my $r (@{$msginfo->per_recip_data}) {
- my($a) = $r->banned_parts;
- if (!defined $a || !@$a) { $banned_filename_all = 0 }
- else {
- $banned_filename_any++;
- my($rhs) = $r->banned_rhs;
- if (defined $rhs) {
- for my $j (0..$#{$a}) {
- if ($rhs->[$j] =~ /^DISCARD/) {
- $banned_dsn_suppress = 1;
- do_log(4,sprintf('BANNED:%s: %s', $rhs->[$j],$rhs->[$j]));
- }
- }
- }
- }
- }
- ll(4) && do_log(4,sprintf("banned check: any=%d, all=%s (%d)",
- $banned_filename_any, $banned_filename_all?'Y':'N',
- scalar(@{$msginfo->per_recip_data})));
- }
- if ($virus_presence_checked) {
- do_log(5, "virus_presence cached, skipping virus_scan");
- } elsif (!$extra_code_antivirus) {
- do_log(5, "no anti-virus code loaded, skipping virus_scan");
- } elsif ($all_bypass_virus_checks) {
- do_log(5, "bypassing of virus checks requested");
- } elsif (defined $hold && $hold ne '') { # protect virus scanner from bombs
- do_log(0, "NOTICE: Virus scanning skipped: $hold");
- $will_do_virus_scanning = 0;
- } else {
- if (!$will_do_virus_scanning)
- { do_log(-1, "NOTICE: will_do_virus_scanning is false???") }
- if (!defined($msginfo->mime_entity)) {
- $which_section = "mime_decode-3";
- my($ent); ($ent,$mime_err) = mime_decode($fh, $tempdir, $parts_root);
- $msginfo->mime_entity($ent);
- prolong_timer($which_section);
- }
- # special case to make available a complete mail file for inspection
- if ((defined($mime_err) && $mime_err ne '') ||
- lookup(0,'MAIL',@keep_decoded_original_maps) ||
- $any_undecipherable && lookup(0,'MAIL-UNDECIPHERABLE',
- @keep_decoded_original_maps)) {
- # keep the original email.txt by making a hard link to it in ./parts/
- $which_section = "linking-to-MAIL";
- my($newpart_obj) =
- Amavis::Unpackers::Part->new("$tempdir/parts",$parts_root,1);
- my($newpart) = $newpart_obj->full_name;
- do_log(2, "providing full original message to scanners as $newpart".
- (!$any_undecipherable ?'' :", $any_undecipherable undecipherable").
- ($mime_err eq '' ? '' : ", MIME error: $mime_err") );
- link($msginfo->mail_text_fn, $newpart)
- or die sprintf("Can't create hard link %s to %s: %s",
- $newpart, $msginfo->mail_text_fn, $!);
- $newpart_obj->type_short('MAIL');
- $newpart_obj->type_declared('message/rfc822');
- }
- $which_section = "virus_scan";
- # some virus scanners behave badly if interrupted,
- # so for now just turn off the timer
- my($remaining_time) = alarm(0); # check time left, stop timer
- my($av_ret);
- eval {
- my($vn, $ds);
- ($av_ret, $av_output, $vn, $ds) =
- Amavis::AV::virus_scan($tempdir, $child_task_count==1, $parts_root);
- @virusname = @$vn; @detecting_scanners = @$ds; # copy
- };
- prolong_timer($which_section, $remaining_time); # restart timer
- if ($@ ne '') {
- chomp($@);
- if ($@ eq "timed out") { # can't happen, timer is stopped
- @virusname = (); $av_ret = 0; # assume not a virus!
- do_log(-1, "virus_scan TIMED OUT, ASSUME NOT A VIRUS !!!");
- } else {
- $hold = "virus_scan: $@"; # request HOLD
- $av_ret = 0; # pretend it was ok (msg should be held)
- die "$hold\n"; # die, TEMPFAIL is preferred to HOLD
- }
- }
- snmp_count('OpsVirusCheck');
- defined($av_ret) or die "All virus scanners failed!";
- @$cache_entry{'Vt','VO','VN','VD'} =
- ($now, $av_output, \@virusname, \@detecting_scanners);
- $virus_presence_checked = 1;
- if (defined $snmp_db && @virusname) {
- $which_section = "read_snmp_variables";
- $virus_dejavu = 1
- if !grep {!defined($_) || $_ == 0} # none with counter zero or undef
- @{$snmp_db->read_snmp_variables(map {"virus.byname.$_"} @virusname)};
- section_time($which_section);
- }
- }
- $which_section = "post_virus_scan";
- if ($virus_presence_checked) {
- my($bpvcm) = ca('bypass_virus_checks_maps');
- for my $r (@{$msginfo->per_recip_data}) {
- $r->infected(lookup(0,$r->recip_addr,@$bpvcm) ? undef :
- @virusname ? 1 : 0);
- }
- }
- my($sender_contact,$sender_source);
- if (!@virusname) { $sender_contact = $sender_source = $msginfo->sender }
- else {
- ($sender_contact,$sender_source) = best_try_originator(
- $msginfo->sender, $msginfo->mime_entity, \@virusname);
- section_time('best_try_originator');
- }
- $msginfo->sender_contact($sender_contact); # save it
- $msginfo->sender_source($sender_source); # save it
- # consider doing spam scanning
- if (!$extra_code_antispam) {
- do_log(5, "no anti-spam code loaded, skipping spam_scan");
- } elsif (@virusname) {
- do_log(5, "infected contents, skipping spam_scan");
- } elsif ($banned_filename_all) {
- do_log(5, "banned contents, skipping spam_scan");
- } elsif (!grep {!lookup(0,$_,@{ca('bypass_spam_checks_maps')})} @recips) {
- do_log(5, "bypassing of spam checks requested");
- } else {
- $which_section = "spam-wb-list";
- my($any_wbl, $all_wbl) = Amavis::SpamControl::white_black_list(
- $conn, $msginfo, $sql_wblist, $user_id_sql, $ldap_policy);
- section_time($which_section);
- if ($all_wbl) {
- do_log(5, "sender white/blacklisted, skipping spam_scan");
- } elsif ($spam_presence_checked) {
- do_log(5, "spam_presence cached, skipping spam_scan");
- } else {
- $which_section = "spam_scan";
- ($spam_level, $spam_status, $spam_report, $autolearn_status) =
- Amavis::SpamControl::spam_scan($conn, $msginfo);
- prolong_timer($which_section);
- snmp_count('OpsSpamCheck');
- @$cache_entry{'St','SL','SS','SR'} =
- ($now, $spam_level, $spam_status, $spam_report);
- $spam_presence_checked = 1;
- }
- }
- # store to cache
- $which_section = 'update_cache';
- $cache_entry->{'atime'} = $now_utc_iso8601; # update accessed timestamp
- $body_digest_cache->set($body_digest,$cache_entry,
- $now_utc_iso8601,$expires_utc_iso8601)
- if $body_digest_cache && defined $body_digest;
- $cache_entry = undef; # discard the object, it is no longer needed
- section_time($which_section);
- snmp_count("virus.byname.$_") for @virusname;
- # SECOND: now that we know what we got, decide what to do with it
- $which_section = 'after_scanning';
- my($considered_spam_by_some_recips,$considered_oversize_by_some_recips);
- if (@virusname || $banned_filename_any) { # virus or banned filename found
- # bad_headers do not enter this section, although code is ready for them;
- # we'll handle bad headers later, if mail turns out not to be spam
- $which_section = "deal_with_virus_or_banned";
- for my $r (@{$msginfo->per_recip_data}) {
- next if $r->recip_done; # already dealt with
- my($final_destiny) = $r->infected ? c('final_virus_destiny')
- : defined($r->banned_parts) && @{$r->banned_parts}
- ? c('final_banned_destiny')
- : @bad_headers ? c('final_bad_header_destiny')
- : D_PASS;
- my($whitelisted_for_malware) = 0;
- # if ($final_destiny != D_PASS && lookup(0,$msginfo->sender,
- # [new_RE(qr'bugtraq-return-.*@securityfocus\.com')] )) {
- # $whitelisted_for_malware = 1;
- # do_log(0, "malware accepted from sender ".$msginfo->sender);
- # }
- if ($final_destiny == D_PASS || $whitelisted_for_malware) {
- # recipient wants this message, malicious or not
- } elsif ((!$r->infected || # not a virus, ignored or we want it
- lookup(0,$r->recip_addr, @{ca('virus_lovers_maps')})) &&
- # not banned or we want it
- (!defined($r->banned_parts) || !@{$r->banned_parts} ||
- lookup(0,$r->recip_addr, @{ca('banned_files_lovers_maps')})) &&
- (!@bad_headers || # not bad header or we want it
- lookup(0,$r->recip_addr, @{ca('bad_header_lovers_maps')})) )
- {
- # clean, not noticed (bypass...), or recipient wants it
- } else { # change mail destiny for those not wanting malware
- $r->recip_destiny($final_destiny);
- my($reason);
- if ($r->infected)
- { $reason = "VIRUS: " . join(", ", @virusname) }
- elsif (defined($r->banned_parts) && @{$r->banned_parts})
- { $reason = "BANNED: " . join(", ", @{$r->banned_parts}) }
- elsif (@bad_headers)
- { $reason = "BAD_HEADER: " . join(", ", @bad_headers) }
- $reason = substr($reason,0,100)."..." if length($reason) > 100+3;
- $r->recip_smtp_response( ($final_destiny == D_DISCARD
- ? "250 2.7.1 Ok, discarded"
- : "550 5.7.1 Message content rejected") .
- ", id=$am_id - $reason");
- $r->recip_done(1);
- # note that 5xx status rejects may later be converted to bounces or
- # discards, according to $*_destiny setting
- }
- }
- $which_section = "virus_or_banned quar+notif";
- ensure_mime_entity($msginfo, $fh, $tempdir, \@virusname, $parts_root);
- # send notifications, quarantine it
- do_virus($conn, $msginfo, $virus_dejavu);
- } else { # perhaps some recips consider it spam?
- # spaminess is an individual matter, we must compare spam level
- # with each recipient setting, there is no single global criterium
- # that the mail is spam
- $which_section = "deal_with_spam";
- my($final_destiny) = c('final_spam_destiny');
- for my $r (@{$msginfo->per_recip_data}) {
- next if $r->recip_done; # already dealt with
- my($kill_level);
- $kill_level = lookup(0,$r->recip_addr, @{ca('spam_kill_level_maps')});
- my($boost) = $r->recip_score_boost;
- $boost = 0 if !defined($boost); # avoid uninitialized value warning
- my($should_be_killed) =
- !$r->recip_whitelisted_sender &&
- ($r->recip_blacklisted_sender ||
- (defined $spam_level && defined $kill_level ?
- $spam_level+$boost >= $kill_level : 0) );
- next unless $should_be_killed;
- # message is at or above kill level, or sender is blacklisted
- $considered_spam_by_some_recips = 1;
- if ($final_destiny == D_PASS ||
- lookup(0,$r->recip_addr, @{ca('spam_lovers_maps')})) {
- # do nothing, recipient wants this message, even if spam
- } else { # change mail destiny for those not wanting spam
- ll(3) && do_log(3,sprintf(
- "SPAM-KILL, %s -> %s, score=%s, kill=%s%s",
- qquote_rfc2821_local($msginfo->sender, $r->recip_addr),
- (!defined $spam_level ? 'x'
- : !defined $boost ? $spam_level
- : $boost >= 0 ? $spam_level.'+'.$boost : $spam_level.$boost),
- !defined $kill_level ? 'x' : 0+sprintf("%.3f",$kill_level),
- $r->recip_blacklisted_sender ? ', BLACKLISTED' : ''));
- $r->recip_destiny($final_destiny);
- my($reason) =
- $r->recip_blacklisted_sender ? 'sender blacklisted' : 'UBE';
- $r->recip_smtp_response(($final_destiny == D_DISCARD
- ? "250 2.7.1 Ok, discarded, $reason"
- : "550 5.7.1 Message content rejected, $reason"
- ) . ", id=$am_id");
- $r->recip_done(1);
- }
- }
- if ($considered_spam_by_some_recips) {
- $which_section = "spam quar+notif";
- ensure_mime_entity($msginfo, $fh, $tempdir, \@virusname, $parts_root);
- do_spam($conn, $msginfo,
- $spam_level, $spam_status, $spam_report, $autolearn_status);
- section_time('post-do_spam');
- }
- }
- if (@bad_headers) { # invalid mail headers
- $which_section = "deal_with_bad_headers";
- ensure_mime_entity($msginfo, $fh, $tempdir, \@virusname, $parts_root);
- my($is_bulk) = $msginfo->mime_entity->head->get('precedence', 0);
- chomp($is_bulk);
- do_log(1,sprintf("BAD HEADER from %s<%s>: %s",
- $is_bulk eq '' ? '' : "($is_bulk) ", $msginfo->sender,
- $bad_headers[0]));
- $is_bulk = $is_bulk=~/^(bulk|list|junk)/i ? $1 : undef;
- my($any_badh); my($final_destiny) = c('final_bad_header_destiny');
- for my $r (@{$msginfo->per_recip_data}) {
- next if $r->recip_done; # already dealt with
- if ($final_destiny == D_PASS ||
- lookup(0,$r->recip_addr, @{ca('bad_header_lovers_maps')}))
- {
- # recipient wants this message, broken or not
- } elsif ($final_destiny == D_BOUNCE &&
- (defined $is_bulk || $msginfo->sender eq '')) {
- # have mercy on mailing lists and DSN: since a bounce for such mail
- # will be suppressed, it is probably better to just let a mail pass
- } else { # change mail destiny for those not wanting it
- $r->recip_destiny($final_destiny);
- my($reason) = (split(/\n/, $bad_headers[0]))[0];
- $r->recip_smtp_response(($final_destiny == D_DISCARD
- ? "250 2.6.0 Ok, message with invalid header discarded"
- : "554 5.6.0 Message with invalid header rejected"
- ) . ", id=$am_id - $reason");
- $r->recip_done(1);
- $any_badh++;
- }
- }
- if ($any_badh) { # we use the same code as for viruses or banned
- # but only if it wasn't already handled as spam
- do_virus($conn, $msginfo, 0); # send notifications, quarantine it
- }
- section_time($which_section);
- }
- my($mslm) = ca('message_size_limit_maps');
- if (@$mslm) {
- $which_section = "deal_with_mail_size";
- my($mail_size) = $msginfo->msg_size;
- for my $r (@{$msginfo->per_recip_data}) {
- next if $r->recip_done; # already dealt with
- my($size_limit) = lookup(0,$r->recip_addr, @$mslm);
- $size_limit = 65536
- if $size_limit && $size_limit < 65536; # rfc2821
- if ($size_limit && $mail_size > $size_limit) {
- do_log(1,sprintf("OVERSIZE from %s to %s: size %s B, limit %s B",
- qquote_rfc2821_local($msginfo->sender),
- qquote_rfc2821_local($r->recip_addr),
- $mail_size, $size_limit))
- if !$considered_oversize_by_some_recips;
- $considered_oversize_by_some_recips = 1;
- $r->recip_destiny(D_BOUNCE);
- $r->recip_smtp_response("552 5.3.4 Message size ($mail_size B) ".
- "exceeds recipient's size limit, id=$am_id");
- $r->recip_done(1);
- }
- }
- section_time($which_section);
- }
- $which_section = "aux_quarantine";
- # do_quarantine($conn, $msginfo, undef,
- # ['archive-files'], 'local:archive-ham/%m.gz'
- # ) unless $considered_oversize_by_some_recips ||
- # ref($msginfo->quarantined_to) && @{$msginfo->quarantined_to};
- # do_quarantine($conn, $msginfo, undef,
- # ['archive-files'], 'local:archive/%m');
- # do_quarantine($conn, $msginfo, undef,
- # ['archive@localhost'], 'local:all-%m');
- # do_quarantine($conn, $msginfo, undef,
- # ['sender-quarantine'], 'local:user-%m'
- # ) if lookup(0,$msginfo->sender, ['user1@domain','user2@domain']);
- # section_time($which_section);
- $which_section = "checking_sender_ip";
- my(@recips) = @{$msginfo->recips};
- if ($considered_spam_by_some_recips && @recips==1 &&
- $recips[0] eq $msginfo->sender &&
- lookup(0,$msginfo->sender, @{ca('local_domains_maps')}))
- { # ad-hoc check for externally originating spam with sender=recipient
- # turns off spam bounce
- my($cl_ip) = $msginfo->client_addr;
- if ($cl_ip eq '') {
- ensure_mime_entity($msginfo, $fh, $tempdir, \@virusname, $parts_root);
- $cl_ip = fish_out_ip_from_received(
- $msginfo->mime_entity->head->get('received',0));
- }
- if ($cl_ip ne '' && !lookup_ip_acl($cl_ip,@{ca('mynetworks_maps')})) {
- do_log(2,"disabling DSN, spam from external source $cl_ip, ".
- "local sender believed to be faked: ".$msginfo->sender);
- $msginfo->sender_contact(undef); # believed to be faked
- }
- }
- if (defined $hold && $hold ne '')
- { do_log(-1, "NOTICE: HOLD reason: $hold") }
- # THIRD: now that we know what to do with it, do it! (deliver or bounce)
- my($which_content_counter) =
- @virusname ? 'ContentVirusMsgs'
- : $banned_filename_any ? 'ContentBannedMsgs'
- : $considered_spam_by_some_recips ? 'ContentSpamMsgs'
- : @bad_headers ? 'ContentBadHdrMsgs'
- : $considered_oversize_by_some_recips ? 'ContentOversizeMsgs'
- : 'ContentCleanMsgs';
- snmp_count($which_content_counter);
- my($hdr_edits) = $msginfo->header_edits;
- if (!$hdr_edits) {
- $hdr_edits = Amavis::Out::EditHeader->new;
- $msginfo->header_edits($hdr_edits);
- }
- if ($msginfo->delivery_method eq '') { # AM.PDP or AM.CL (milter)
- $which_section = "AM.PDP headers";
- ensure_mime_entity($msginfo, $fh, $tempdir, \@virusname, $parts_root);
- $hdr_edits = add_forwarding_header_edits_common(
- $conn, $msginfo, $hdr_edits, $hold, $any_undecipherable,
- $virus_presence_checked, $spam_presence_checked,
- $spam_level, $spam_status, $spam_report, $autolearn_status,
- undef);
- my($done_all);
- my($recip_cl); # ref to a list of similar recip objects
- ($hdr_edits, $recip_cl, $done_all) =
- add_forwarding_header_edits_per_recip(
- $conn, $msginfo, $hdr_edits, $hold, $any_undecipherable,
- $virus_presence_checked, $spam_presence_checked,
- $spam_level, $spam_status, $spam_report, $autolearn_status,
- undef, undef);
- $msginfo->header_edits($hdr_edits); # store edits (redundant?)
- if (@$recip_cl && !$done_all) {
- do_log(-1, "AM.PDP: CLIENTS REQUIRE DIFFERENT HEADERS");
- };
- } elsif (grep { !$_->recip_done } @{$msginfo->per_recip_data}) { # forward
- # To be delivered explicitly - only to those recipients not yet marked
- # as 'done' by the above content filtering sections.
- $which_section = "forwarding";
- ensure_mime_entity($msginfo, $fh, $tempdir, \@virusname, $parts_root);
- # a quick-fix solution to defang dangerous contents
- my($mail_defanged); # nonempty indicates mail body is replaced
- my($explanation); my($defang_all) = c('defang_all');
- if ($hold ne '') { $explanation =
- "WARNING: possible mail bomb, NOT CHECKED FOR VIRUSES:\n $hold";
- } elsif (@virusname) {
- $explanation = 'WARNING: contains virus '.join(' ',@virusname)
- if c('defang_virus') || $defang_all;
- } elsif ($banned_filename_any) {
- $explanation = "WARNING: contains banned part"
- if c('defang_banned') || $defang_all;
- } elsif ($any_undecipherable) {
- $explanation = "WARNING: contains undecipherable part"
- if c('defang_undecipherable') || $defang_all;
- } elsif ($considered_spam_by_some_recips) {
- $explanation = $spam_report
- if c('defang_spam') || $defang_all;
- } elsif (@bad_headers) {
- $explanation = 'WARNING: bad headers '.join(' ',@bad_headers)
- if c('defang_bad_header') || $defang_all;
- } else { $explanation = '(clean)' if $defang_all }
- if (defined $explanation) { # malware
- $explanation .= "\n" if $explanation !~ /\n\z/;
- my($s) = $explanation; $s=~s/[ \t\n]+\z//;
- if (length($s) > 100) { $s = substr($s,0,100-3) . "..." }
- do_log(1, "DEFANGING MAIL: $s");
- my($d) = defanged_mime_entity($conn,$msginfo,$explanation);
- $msginfo->mail_text($d); # substitute mail with rewritten version
- $msginfo->mail_text_fn(undef); # remove filename information
- $mail_defanged = 'Original mail wrapped as attachment (defanged)';
- section_time('defang');
- }
- $hdr_edits = add_forwarding_header_edits_common(
- $conn, $msginfo, $hdr_edits, $hold, $any_undecipherable,
- $virus_presence_checked, $spam_presence_checked,
- $spam_level, $spam_status, $spam_report, $autolearn_status,
- $mail_defanged);
- for (;;) { # do the delivery
- my($r_hdr_edits) = Amavis::Out::EditHeader->new; # per-recip edits set
- $r_hdr_edits->inherit_header_edits($hdr_edits);
- my($done_all);
- my($recip_cl); # ref to a list of similar recip objects
- ($r_hdr_edits, $recip_cl, $done_all) =
- add_forwarding_header_edits_per_recip(
- $conn, $msginfo, $r_hdr_edits, $hold, $any_undecipherable,
- $virus_presence_checked, $spam_presence_checked,
- $spam_level, $spam_status, $spam_report, $autolearn_status,
- $mail_defanged, undef);
- last if !@$recip_cl;
- $msginfo->header_edits($r_hdr_edits); # store edits
- mail_dispatch($conn, $msginfo, 0, $dsn_per_recip_capable,
- sub { my($r) = @_; grep { $_ eq $r } @$recip_cl });
- snmp_count('OutForwMsgs');
- snmp_count('OutForwHoldMsgs') if $hold ne '';
- $point_of_no_return = 1; # now past the point where mail was sent
- last if $done_all;
- }
- }
- prolong_timer($which_section);
- $which_section = "delivery-notification";
- my($dsn_needed); my($warnsender_with_pass,$which_dsn_counter,$dsnmsgref);
- ($smtp_resp, $exit_code, $dsn_needed) =
- one_response_for_all($msginfo, $dsn_per_recip_capable, $am_id);
- if ($smtp_resp =~ /^2/ && !$dsn_needed) {
- ($warnsender_with_pass,$which_dsn_counter,$dsnmsgref) =
- @virusname && c('warnvirussender') ?
- (1, 'OutDsnVirusMsgs', cr('notify_virus_sender_templ'))
- : $banned_filename_any && c('warnbannedsender') ?
- (1, 'OutDsnBannedMsgs', cr('notify_virus_sender_templ'))
- : $considered_spam_by_some_recips && c('warnspamsender') ?
- (1, 'OutDsnSpamMsgs', cr('notify_spam_sender_templ'))
- : @bad_headers && c('warnbadhsender') ?
- (1, 'OutDsnBadHdrMsgs', cr('notify_sender_templ')) : (0,undef,undef);
- }
- ll(4) && do_log(4,sprintf(
- "warnsender_with_pass=%s (%s,%s,%s,%s), ".
- "dsn_needed=%s, cnt=%s, exit=%s, %s",
- map {defined $_ ? $_ : ''} ( # avoid warnings about uninitialized value
- $warnsender_with_pass,
- c('warnvirussender'),c('warnbannedsender'),
- c('warnbadhsender'),c('warnspamsender'),
- $dsn_needed,$which_dsn_counter,$exit_code,$smtp_resp) ));
- if ($dsn_needed || $warnsender_with_pass) {
- ensure_mime_entity($msginfo, $fh, $tempdir, \@virusname, $parts_root);
- my($what_bad_content) = join(' & ',
- !@virusname ? () : 'VIRUS',
- !$banned_filename_any ? () : 'BANNED',
- !$considered_spam_by_some_recips ? () : 'SPAM',
- !@bad_headers ? () : 'BAD HEADER',
- !$considered_oversize_by_some_recips ? () : 'OVERSIZE');
- my($notification); my($dsn_cutoff_level);
- if ($msginfo->sender eq '') { # don't respond to null reverse path
- my($msg) = "DSN contains $what_bad_content; bounce is not bouncible";
- if (!$dsn_needed) { do_log(4, $msg) }
- else { do_log(1, "NOTICE: $msg, mail intentionally dropped") }
- $msginfo->dsn_sent(2); # pretend the message was bounced
- } elsif ($msginfo->sender_contact eq '') {
- my($msg) = sprintf("Not sending DSN to believed-to-be-faked "
- . "sender <%s>, mail containing %s",
- $msginfo->sender, $what_bad_content);
- if (!$dsn_needed) { do_log(4, $msg) }
- else { do_log(2, "NOTICE: $msg intentionally dropped") }
- $msginfo->dsn_sent(2); # pretend the message was bounced
- } elsif ($banned_dsn_suppress) {
- my($msg) = "Not sending DSN, as suggested by banned rule";
- if (!$dsn_needed) { do_log(4, $msg) }
- else { do_log(1, "NOTICE: $msg, mail intentionally dropped") }
- $msginfo->dsn_sent(2); # pretend the message was bounced
- } elsif (defined $spam_level &&
- !grep { $dsn_cutoff_level = lookup(0,$_->recip_addr,
- @{ca('spam_dsn_cutoff_level_maps')}),
- !defined($dsn_cutoff_level) ||
- $spam_level + $_->recip_score_boost < $dsn_cutoff_level
- } @{$msginfo->per_recip_data} ) {
- my($msg) = "Not sending DSN, spam level exceeds DSN cutoff level for all recips";
- if (!$dsn_needed) { do_log(4, $msg) }
- else { do_log(1, "NOTICE: $msg, mail intentionally dropped") }
- $msginfo->dsn_sent(2); # pretend the message was bounced
- } elsif ((@virusname || $banned_filename_any ||
- $considered_spam_by_some_recips || @bad_headers ||
- $considered_oversize_by_some_recips) &&
- $msginfo->mime_entity->head->get('precedence',0)
- =~ /^(bulk|list|junk)/i )
- { my($msg) = sprintf("Not sending DSN in response to bulk mail "
- . "from <%s> containing %s",
- $msginfo->sender, $what_bad_content);
- if (!$dsn_needed) { do_log(4, $msg) }
- else { do_log(1, "NOTICE: $msg, mail intentionally dropped") }
- $msginfo->dsn_sent(2); # pretend the message was bounced
- } else { # prepare a notification
- ### TODO: better selection of DSN reason is still needed!
- if (!$warnsender_with_pass) { # it will be a non-delivery notification
- my($prio) = 0; # choose the most relevant notification template
- for my $r (@{$msginfo->per_recip_data}) {
- local($_) = $r->recip_done ? $r->recip_smtp_response : $smtp_resp;
- my($t_prio,$t_which_dsn_counter,$t_dsnmsgref) =
- /^([25]).*\bVIRUS\b/ ?
- ($1*10+5, 'OutDsnVirusMsgs', cr('notify_virus_sender_templ'))
- : /^([25]).*\bBANNED\b/ ?
- ($1*10+4, 'OutDsnBannedMsgs',cr('notify_virus_sender_templ'))
- : /^([25]).*\b(?:UBE|blacklisted)\b/ ?
- ($1*10+3, 'OutDsnSpamMsgs', cr('notify_spam_sender_templ'))
- : /^([25]).*\bheader\b/ ?
- ($1*10+2, 'OutDsnBadHdrMsgs',cr('notify_sender_templ'))
- : (0, undef, undef);
- ($prio,$which_dsn_counter,$dsnmsgref) =
- ($t_prio,$t_which_dsn_counter,$t_dsnmsgref) if $t_prio > $prio;
- }
- }
- ($which_dsn_counter,$dsnmsgref) =
- ('OutDsnOtherMsgs',cr('notify_sender_templ')) if !defined $dsnmsgref;
- do_log(4,"notification chosen: $which_dsn_counter, $dsnmsgref");
- # generate delivery status notification according to rfc3462 & rfc3464
- $notification = delivery_status_notification($conn, $msginfo,
- $warnsender_with_pass, \%builtins, $dsnmsgref) if $dsnmsgref;
- snmp_count($which_dsn_counter) if defined $notification;
- }
- if (defined $notification) { # dsn needed, send delivery notification
- mail_dispatch($conn, $notification, 1, 0);
- snmp_count('OutDsnMsgs');
- my($n_smtp_resp, $n_exit_code, $n_dsn_needed) =
- one_response_for_all($notification, 0, $am_id); # check status
- if ($n_smtp_resp =~ /^2/ && !$n_dsn_needed) { # dsn successful?
- $msginfo->dsn_sent(1); # mark the message as bounced
- $point_of_no_return = 2; # now past the point where DSN was sent
- } elsif ($n_smtp_resp =~ /^4/) {
- snmp_count('OutDsnTempFails');
- die sprintf("temporarily unable to send DSN to <%s>: %s",
- $msginfo->sender_contact, $n_smtp_resp);
- } else {
- snmp_count('OutDsnRejects');
- do_log(-1,sprintf("NOTICE: UNABLE TO SEND DSN to <%s>: %s",
- $msginfo->sender, $n_smtp_resp));
- # # if dsn can not be sent, try to send it to postmaster
- # $notification->recips(['postmaster']);
- # # attempt double bounce
- # mail_dispatch($conn, $notification, 1, 0);
- }
- # $notification->purge;
- }
- }
- prolong_timer($which_section);
- # generate customized log report at log level 0 - this is usually the
- # only log entry interesting to administrators during normal operation
- $which_section = 'main_log_entry';
- my(%mybuiltins) = %builtins; # make a local copy
- { # do a per-message log entry
- my($s) = $spam_status;
- $s =~ s/^tests=\[ ( [^\]]* ) \]/$1/x; my(@s) = split(/,/,$s);
- if (@s > 50) { $#s = 50-1; push(@s,"...") } # arbitrary sanity limit
- $mybuiltins{'T'} = \@s; # macro %T has overloaded semantics, ugly
- my($y,$n,$f) = delivery_short_report($msginfo);
- @mybuiltins{'D','O','N'} = ($y,$n,$f);
- my($strr) = expand(cr('log_templ'), \%mybuiltins);
- for my $logline (split(/[ \t]*\n/, $$strr)) {
- do_log(0, $logline) if $logline ne '';
- }
- }
- if (c('log_recip_templ') ne '') { # do per-recipient log entries
- # redefine macros with a by-recipient semantics
- for my $r (@{$msginfo->per_recip_data}) {
- # recipient counter in macro %. may indicate to the template
- # that a per-recipient expansion semantics is expected
- $mybuiltins{'.'}++;
- my($recip) = $r->recip_addr;
- my($smtp_resp) = $r->recip_smtp_response;
- my($qrecip_addr) = scalar(qquote_rfc2821_local($recip));
- $mybuiltins{'D'} = $mybuiltins{'O'} = $mybuiltins{'N'} = undef;
- if ($r->recip_destiny==D_PASS && ($smtp_resp=~/^2/ || !$r->recip_done)){
- $mybuiltins{'D'} = $qrecip_addr;
- } else {
- $mybuiltins{'O'} = $qrecip_addr;
- my($remote_mta) = $r->recip_remote_mta;
- $mybuiltins{'N'} = sprintf("%s:%s\n %s", $qrecip_addr,
- ($remote_mta eq '' ? '' : " $remote_mta said:"), $smtp_resp);
- }
- my(@b); @b = @{$r->banned_parts} if defined $r->banned_parts;
- my($b_chopped) = @b > 2; @b = (@b[0,1],'...') if $b_chopped;
- s/[ \t]{6,}/ ... /g for @b;
- $mybuiltins{'F'} = \@b; # list of banned file names
- my($blacklisted) = $r->recip_blacklisted_sender;
- my($whitelisted) = $r->recip_whitelisted_sender;
- my($boost) = $r->recip_score_boost;
- my($is_local,$tag_level,$tag2_level,$kill_level);
- $is_local = lookup(0,$recip, @{ca('local_domains_maps')});
- $tag_level = lookup(0,$recip, @{ca('spam_tag_level_maps')});
- $tag2_level = lookup(0,$recip, @{ca('spam_tag2_level_maps')});
- $kill_level = lookup(0,$recip, @{ca('spam_kill_level_maps')});
- my($do_tag) =
- $blacklisted || !defined $tag_level ||
- (defined $spam_level ? $spam_level+$boost >= $tag_level
- : $whitelisted ? (-10 >= $tag_level) : 0);
- my($do_tag2) = !$whitelisted &&
- ( $blacklisted ||
- (defined $spam_level && defined $tag2_level ?
- $spam_level+$boost >= $tag2_level : 0) );
- my($do_kill) = !$whitelisted &&
- ( $blacklisted ||
- (defined $spam_level && defined $kill_level ?
- $spam_level+$boost >= $kill_level : 0) );
- for ($do_tag,$do_tag2,$do_kill) { $_ = $_ ? 'Y' : '0' } # normalize
- for ($is_local) { $_ = $_ ? 'L' : '0' } # normalize
- for ($tag_level,$tag2_level,$kill_level) { $_ = 'x' if !defined($_) }
- $mybuiltins{'R'} = $recip;
- $mybuiltins{'c'} = do { # format SA score +/- by-sender score boost
- if (!defined($spam_level)) { '-' }
- else {
- my($sl) = 0+sprintf("%.3f",$spam_level); # trim down fraction
- my($b) = !defined $boost ? undef : 0+sprintf("%.3f",$boost);
- !defined $boost || $boost == 0 ? $sl
- : $boost >= 0 ? $sl.'+'.$b : $sl.$b;
- }
- };
- @mybuiltins{('0','1','2','k')} = ($is_local,$do_tag,$do_tag2,$do_kill);
- # macros %3, %4, %5 are experimental, until a better solution is found
- @mybuiltins{('3','4','5')} = ($tag_level,$tag2_level,$kill_level);
- my($strr) = expand(cr('log_recip_templ'), \%mybuiltins);
- for my $logline (split(/[ \t]*\n/, $$strr)) {
- do_log(0, $logline) if $logline ne '';
- }
- }
- }
- section_time($which_section);
- if ($sql_storage) { # save final information to SQL (if enabled)
- $which_section = 'sql-update';
- my($ds) = $msginfo->dsn_sent;
- $ds = !$ds ? 'N' : $ds==1 ? 'Y' : $ds==2 ? 'q' : '?';
- my($ct) = @virusname ? 'V' : $banned_filename_any ? 'B' :
- $considered_spam_by_some_recips ? 'S' : @bad_headers ? 'H' :
- $considered_oversize_by_some_recips ? 'O' : 'C';
- for (my($attempt)=5; $attempt>0; ) { # sanity limit on retries
- $sql_storage->save_info_final($conn,$msginfo,$spam_level,$ds,$ct)
- and last;
- if (--$attempt <= 0) {
- do_log(-2,"ERROR sql_storage: too many retries ".
- "on storing final, info not saved");
- } else {
- do_log(2,"sql_storage: retrying on final, $attempt attempts remain");
- sleep(int(1+rand(3))); # can't mix Time::HiRes::sleep with alarm
- }
- };
- section_time($which_section);
- }
- if (defined $snmp_db) {
- $which_section = 'update_snmp';
- snmp_count( ['entropy',0,'STR'] );
- $snmp_db->update_snmp_variables;
- section_time($which_section);
- }
- $which_section = 'finishing';
- }; # end eval
- if ($@ ne '') {
- chomp($@);
- $preserve_evidence = 1;
- my($msg) = "$which_section FAILED: $@";
- if ($point_of_no_return) {
- do_log(-2, "TROUBLE in check_mail, ".
- "but must continue ($point_of_no_return): $msg");
- } else {
- do_log(-2, "TROUBLE in check_mail: $msg");
- $smtp_resp = "451 4.5.0 Error in processing, id=$am_id, $msg";
- $exit_code = EX_TEMPFAIL;
- for my $r (@{$msginfo->per_recip_data})
- { $r->recip_smtp_response($smtp_resp); $r->recip_done(1) }
- }
- }
- # if ($hold ne '') {
- # do_log(-1, "NOTICE: Evidence is to be preserved: $hold");
- # $preserve_evidence = 1;
- # }
- if (!$preserve_evidence && debug_oneshot()) {
- do_log(0, "DEBUG_ONESHOT CAUSES EVIDENCE TO BE PRESERVED");
- $preserve_evidence = 1;
- }
- my($which_counter) = 'InUnknown';
- if ($smtp_resp =~ /^4/) { $which_counter = 'InTempFails' }
- elsif ($smtp_resp =~ /^5/) { $which_counter = 'InRejects' }
- elsif ($smtp_resp =~ /^2/) {
- my($dsn_sent) = $msginfo->dsn_sent;
- if (!$dsn_sent) { $which_counter = $msginfo->delivery_method ne ''
- ? 'InAccepts' : 'InContinues' }
- elsif ($dsn_sent==1) { $which_counter = 'InBounces' }
- elsif ($dsn_sent==2) { $which_counter = 'InDiscards' }
- }
- snmp_count($which_counter);
- $snmp_db->register_proc('.') if defined $snmp_db; # content checking done
- $MSGINFO = undef; # release global reference to msginfo object
- ($smtp_resp, $exit_code, $preserve_evidence);
- }
- # Ensure we have $msginfo->$entity defined when we expect we'll need it,
- # e.g. to construct notifications. While at it, also get us some additional
- # information on sender from the header.
- #
- sub ensure_mime_entity($$$$$) {
- my($msginfo, $fh, $tempdir, $virusname_list, $parts_root) = @_;
- if (!defined($msginfo->mime_entity)) {
- # header may not have been parsed yet, e.g. if the result was cached
- my($ent,$mime_err) = mime_decode($fh, $tempdir, $parts_root);
- $msginfo->mime_entity($ent);
- prolong_timer("ensure_mime_entity");
- }
- }
- sub add_forwarding_header_edits_common($$$$$$$$$$$$) {
- my($conn, $msginfo, $hdr_edits, $hold, $any_undecipherable,
- $virus_presence_checked, $spam_presence_checked,
- $spam_level, $spam_status, $spam_report, $autolearn_status,
- $mail_defanged) = @_;
- $hdr_edits->prepend_header('Received',
- received_line($conn,$msginfo,am_id(),1), 1)
- if $insert_received_line && $msginfo->delivery_method ne '';
- # discard existing X-Amavis-Hold header field, only allow our own
- $hdr_edits->delete_header('X-Amavis-Hold');
- if ($hold ne '') {
- $hdr_edits->append_header('X-Amavis-Hold', $hold);
- do_log(-1, "Inserting header field: X-Amavis-Hold: $hold");
- }
- if ($mail_defanged ne '') {
- # prepend Resent-* header fields, they must precede
- # corresponding Received header field (pushed in reverse order)
- $hdr_edits->prepend_header('Resent-Message-ID',
- sprintf('<RE%s@%s>',$msginfo->mail_id,$myhostname) );
- $hdr_edits->prepend_header('Resent-Date',
- rfc2822_timestamp($msginfo->rx_time));
- $hdr_edits->prepend_header('Resent-From', c('hdrfrom_notify_recip'));
- # append X-Amavis-Modified
- my($msg) = "$mail_defanged by $myhostname";
- $hdr_edits->append_header('X-Amavis-Modified', $msg);
- do_log(1, "Inserting header field: X-Amavis-Modified: $msg");
- }
- if ($extra_code_antivirus) {
- $hdr_edits->delete_header('X-Amavis-Alert');
- $hdr_edits->delete_header(c('X_HEADER_TAG'))
- if c('remove_existing_x_scanned_headers') &&
- (c('X_HEADER_LINE') ne '' && c('X_HEADER_TAG') =~ /^[!-9;-\176]+\z/);
- }
- if ($extra_code_antispam) {
- if (c('remove_existing_spam_headers')) {
- my(@which_headers) = qw(
- X-Spam-Status X-Spam-Level X-Spam-Flag X-Spam-Score
- X-Spam-Report X-Spam-Checker-Version X-Spam-Tests);
- push(@which_headers, qw(
- X-DSPAM-Result X-DSPAM-Confidence X-DSPAM-Probability
- X-DSPAM-Signature X-DSPAM-User X-DSPAM-Factors)) if defined $dspam;
- for my $h (@which_headers) { $hdr_edits->delete_header($h) }
- }
- # $hdr_edits->append_header('X-Spam-Checker-Version',
- # sprintf("SpamAssassin %s (%s) on %s", Mail::SpamAssassin::Version(),
- # $Mail::SpamAssassin::SUB_VERSION, $myhostname));
- }
- $hdr_edits;
- }
- # Prepare header edits for the first not-yet-done recipient.
- # Inspect remaining recipients, returning the list of recipient objects
- # that are receiving the same set of header edits (so the message may be
- # delivered to them in one SMTP transaction).
- #
- sub add_forwarding_header_edits_per_recip($$$$$$$$$$$$$) {
- my($conn, $msginfo, $hdr_edits, $hold, $any_undecipherable,
- $virus_presence_checked, $spam_presence_checked,
- $spam_level, $spam_status, $spam_report, $autolearn_status,
- $mail_defanged, $filter) = @_;
- my(@recip_cluster);
- my(@per_recip_data) = grep { !$_->recip_done && (!$filter || &$filter($_)) }
- @{$msginfo->per_recip_data};
- my($per_recip_data_len) = scalar(@per_recip_data);
- my($first) = 1; my($cluster_key); my($cluster_full_spam_status);
- for my $r (@per_recip_data) {
- my($recip) = $r->recip_addr;
- my($is_local,$blacklisted,$whitelisted,$boost,$tag_level,$tag2_level,
- $do_tag_virus_checked,$do_tag_virus,$do_tag_banned,$do_tag_badh,
- $do_tag,$do_tag2,$do_subj,$do_subj_u,$subject_tag,$subject_tag2,$bypassed);
- $is_local = lookup(0,$recip, @{ca('local_domains_maps')});
- $do_tag_badh = @bad_headers &&
- !lookup(0,$recip,@{ca('bypass_header_checks_maps')});
- $do_tag_banned= defined($r->banned_parts) && @{$r->banned_parts};
- $do_tag_virus = $r->infected; # 1, 0, or undef
- $do_tag_virus_checked = defined($do_tag_virus) &&
- (c('X_HEADER_LINE') ne '' && c('X_HEADER_TAG') =~ /^[!-9;-\176]+\z/);
- if ($extra_code_antispam) {
- # my($bypassed);
- $blacklisted = $r->recip_blacklisted_sender;
- $whitelisted = $r->recip_whitelisted_sender;
- $boost = $r->recip_score_boost;
- $bypassed = lookup(0,$recip, @{ca('bypass_spam_checks_maps')});
- $tag_level = lookup(0,$recip, @{ca('spam_tag_level_maps')});
- $tag2_level = lookup(0,$recip, @{ca('spam_tag2_level_maps')});
- # spam-related headers should _not_ be inserted for:
- # - nonlocal recipients (outgoing mail), as a matter of courtesy
- # to our users;
- # - recipients matching bypass_spam_checks: even though spam checking
- # may have been done for other reasons, these recipients do not
- # expect such headers, so let's pretend the check has not been done
- # and not insert spam-related headers for them
- $do_tag = $is_local && !$bypassed &&
- ( $blacklisted || !defined $tag_level ||
- (defined $spam_level ? $spam_level+$boost >= $tag_level
- : $whitelisted ? (-10 >= $tag_level) : 0) );
- $do_tag2 = $is_local && !$bypassed && !$whitelisted &&
- ( $blacklisted ||
- (defined $spam_level && defined $tag2_level ?
- $spam_level+$boost >= $tag2_level : 0) );
- $subject_tag2 = !$do_tag2 ? undef
- : lookup(0,$recip, @{ca('spam_subject_tag2_maps')});
- $subject_tag = !($do_tag||$do_tag2) ? undef
- : lookup(0,$recip, @{ca('spam_subject_tag_maps')});
- $do_subj = ($subject_tag2 ne '' || $subject_tag ne '') &&
- lookup(0,$recip, @{ca('spam_modifies_subj_maps')});
- }
- if ($hold ne '' || $any_undecipherable) { # adding *UNCHECKED* subject tag?
- $do_subj_u = $is_local && !$r->infected &&
- c('undecipherable_subject_tag') ne '';
- }
- # normalize
- for ($do_tag_virus_checked, $do_tag_virus, $do_tag_banned, $do_tag_badh,
- $do_tag, $do_tag2, $do_subj, $do_subj_u, $is_local) { $_ = $_?1:0 }
- my($spam_level_bar, $full_spam_status);
- if ($do_tag || $do_tag2) {
- my($slc) = c('sa_spam_level_char');
- $spam_level_bar =
- $slc x min($blacklisted ? 64 : $spam_level+$boost, 64) if $slc ne '';
- my($s) = $spam_status; $s =~ s/,/,\n /g; # allow header field wrapping
- $full_spam_status = sprintf("%s,\n score=%s\n%s%s %s%s",
- ($do_tag2 || $do_tag) ? 'Yes' : 'No', #added by awi to get spamflag for yellow
- !defined $spam_level ? 'x' : 0+sprintf("%.3f",$spam_level+$boost),
- !defined $tag_level ? '' : sprintf(" tagged_above=%s\n",$tag_level),
- !defined $tag2_level ? '' : sprintf(" required=%s\n", $tag2_level),
- join('', $blacklisted ? "BLACKLISTED\n " : (),
- $whitelisted ? "WHITELISTED\n " : ()),
- $s);
- } elsif (!$bypassed) {
- my($slc) = c('sa_spam_level_char');
- $spam_level_bar =
- $slc x min($blacklisted ? 64 : $spam_level+$boost, 64) if $slc ne '';
- my($s) = $spam_status; $s =~ s/,/,\n /g; # allow header field wrapping
- $full_spam_status = sprintf("%s,\n score=%s\n%s%s %s%s",
- ($do_tag2 || $do_tag) ? 'Yes' : 'No', #added by awi to get spamflag for yellow
- !defined $spam_level ? 'x' : 0+sprintf("%.3f",$spam_level+$boost),
- !defined $tag_level ? '' : sprintf(" tagged_above=%s\n",$tag_level),
- !defined $tag2_level ? '' : sprintf(" required=%s\n", $tag2_level),
- join('', $blacklisted ? "BLACKLISTED\n " : (),
- $whitelisted ? "WHITELISTED\n " : ()),
- $s);
- }
- my($subject_insert); # concatenation of triggered subject tag strings
- if ($do_subj || $do_subj_u) {
- if ($do_subj_u) {
- $subject_insert = c('undecipherable_subject_tag');
- do_log(3,"adding $subject_insert, $any_undecipherable, $hold");
- }
- if ($do_subj) {
- $subject_insert .= $do_tag2 && $subject_tag2 ne '' ? $subject_tag2
- : $subject_tag;
- }
- }
- my($key) = join("\000", map {defined $_ ? $_ : ''} (
- $do_tag_virus_checked, $do_tag_virus, $do_tag_banned, $do_tag_badh,
- $do_tag, $do_tag2, $do_subj, $do_subj_u, $subject_insert,
- $spam_level_bar, $full_spam_status) );
- if ($first) {
- ll(4) && do_log(4,sprintf(
- "headers CLUSTERING: NEW CLUSTER <%s>: ".
- "score=%s, tag=%s, tag2=%s, subj=%s, subj_u=%s, local=%s, bl=%s, s=%s",
- $recip,
- (!defined $spam_level ? 'x'
- : !defined $boost ? $spam_level
- : $boost >= 0 ? $spam_level.'+'.$boost : $spam_level.$boost),
- $do_tag, $do_tag2, $do_subj, $do_subj_u, $is_local, $blacklisted,
- $subject_insert));
- $cluster_key = $key; $cluster_full_spam_status = $full_spam_status;
- } elsif ($key eq $cluster_key) {
- do_log(5,"headers CLUSTERING: <$recip> joining cluster");
- } else {
- do_log(5,"headers CLUSTERING: skipping <$recip> (tag=$do_tag, tag2=$do_tag2)");
- next; # this recipient will be handled in some later pass
- }
- if ($first) { # insert headers required for the new cluster
- if ($do_tag_virus_checked) {
- $hdr_edits->append_header(c('X_HEADER_TAG'), c('X_HEADER_LINE'));
- }
- if ($do_tag_virus) {
- $hdr_edits->append_header('X-Amavis-Alert',
- "INFECTED, message contains virus:\n " . join(",\n ",@virusname), 1);
- }
- if ($do_tag_banned) {
- my(@b); @b = @{$r->banned_parts} if defined $r->banned_parts;
- my($b_chopped) = @b > 2; @b = (@b[0,1],'...') if $b_chopped;
- my($msg) = "BANNED, message contains " . (@b==1 ? 'part' : 'parts') .
- ":\n " . join(",\n ", @b) . ($b_chopped ? ", ..." : "");
- $msg =~ s/[ \t]{6,}/ ... /g;
- $hdr_edits->append_header('X-Amavis-Alert', $msg, 1);
- }
- if ($do_tag_badh) {
- $hdr_edits->append_header('X-Amavis-Alert',
- 'BAD HEADER '.$bad_headers[0], 1);
- }
- if ($do_tag) {
- $hdr_edits->append_header('X-Spam-Status', $full_spam_status, 1);
- $hdr_edits->append_header('X-Spam-Flag', 'YES');
- $hdr_edits->append_header('X-Spam-Score',
- !defined $spam_level ? '-' : 0+sprintf("%.3f",$spam_level+$boost) );
- $hdr_edits->append_header('X-Spam-Level',
- $spam_level_bar) if defined $spam_level_bar;
- } elsif ($do_tag2) {
- $hdr_edits->append_header('X-Spam-Status', $full_spam_status, 1);
- $hdr_edits->append_header('X-Spam-Flag', 'YES');
- $hdr_edits->append_header('X-Spam-Score',
- !defined $spam_level ? '-' : 0+sprintf("%.3f",$spam_level+$boost) );
- $hdr_edits->append_header('X-Spam-Level',
- $spam_level_bar) if defined $spam_level_bar;
- $hdr_edits->append_header('X-Spam-Report', $spam_report,1)
- if $spam_report ne '' && c('sa_spam_report_header');
- } elsif (!$bypassed) {
- $hdr_edits->append_header('X-Spam-Status', $full_spam_status, 1);
- $hdr_edits->append_header('X-Spam-Level',
- $spam_level_bar) if defined $spam_level_bar;
- }
- if ($do_subj || $do_subj_u) {
- my($entity) = $msginfo->mime_entity;
- if (defined $entity && defined $entity->head->get('Subject',0)) {
- $hdr_edits->edit_header('Subject',
- sub { $_[1]=~/^([ \t]?)(.*)\z/s; ' '.$subject_insert.$2 });
- } else { # no Subject header field present, insert one
- $subject_insert =~ s/[ \t]+\z//; # trim
- $hdr_edits->append_header('Subject', $subject_insert);
- if (!defined $entity) {
- do_log(-1,"WARN: no MIME entity!? Inserting 'Subject'");
- } else {
- do_log(0,"INFO: no existing header field 'Subject', inserting it");
- }
- }
- }
- }
- push(@recip_cluster,$r); $first = 0;
- my($delim) = c('recipient_delimiter');
- if ($delim ne '' && $is_local) {
- # append address extensions to mailbox names if desired
- my($ext_map) = $do_tag_virus ? ca('addr_extension_virus_maps')
- : $do_tag_banned ? ca('addr_extension_banned_maps')
- : $do_tag2 ? ca('addr_extension_spam_maps')
- : $do_tag_badh ? ca('addr_extension_bad_header_maps')
- : undef;
- my($ext) = !ref($ext_map) ? undef : lookup(0,$recip, @$ext_map);
- if ($ext ne '') {
- my($orig_extension); my($localpart,$domain) = split_address($recip);
- ($localpart,$orig_extension) = split_localpart($localpart,$delim)
- if c('replace_existing_extension'); # strip existing extension
- my($new_addr) = $localpart.$delim.$ext.$domain;
- ll(5) && do_log(5, (!defined($orig_extension) ? "appending addr ext"
- : "replacing addr ext '$orig_extension' by")
- . " '$ext', giving '$new_addr'");
- $r->recip_addr_modified($new_addr);
- }
- }
- }
- my($done_all);
- if (@recip_cluster == $per_recip_data_len) {
- do_log(5,"headers CLUSTERING: " .
- "done all $per_recip_data_len recips in one go");
- $done_all = 1;
- } else {
- ll(4) && do_log(4,sprintf(
- "headers CLUSTERING: got %d recips out of %d: %s",
- scalar(@recip_cluster), $per_recip_data_len,
- join(", ", map { "<" . $_->recip_addr . ">" } @recip_cluster) ));
- }
- my($s) = $cluster_full_spam_status; $s =~ s/\n[ \t]/ /g;
- ll(2) && do_log(2,sprintf("SPAM-TAG, %s -> %s, %s",
- qquote_rfc2821_local($msginfo->sender),
- join(',', qquote_rfc2821_local(
- map { $_->recip_addr } @recip_cluster)), $s));
- ($hdr_edits, \@recip_cluster, $done_all);
- }
- sub do_quarantine($$$$$;$) {
- my($conn,$msginfo,$hdr_edits,$recips_ref,$quarantine_method,$snmp_id) = @_;
- if ($quarantine_method eq '') { do_log(5, "quarantine disabled") }
- else {
- my($sender) = $msginfo->sender;
- my($quar_msg) = Amavis::In::Message->new;
- $quar_msg->rx_time($msginfo->rx_time); # copy the reception time
- $quar_msg->body_type($msginfo->body_type); # use the same BODY= type
- $quar_msg->mail_id($msginfo->mail_id); # use the same the mail_id
- $quar_msg->body_digest($msginfo->body_digest); # copy original digest
- $quar_msg->delivery_method($quarantine_method);
- if ($quarantine_method =~ /^(bsmtp|sql):/i) {
- $quar_msg->sender($sender); # original sender & recipients
- $quar_msg->recips($msginfo->recips);
- } else {
- my($mftq) = c('mailfrom_to_quarantine');
- $quar_msg->sender(defined $mftq ? $mftq : $sender);
- $quar_msg->recips($recips_ref); # e.g. per-recip quarantine
- }
- $hdr_edits = Amavis::Out::EditHeader->new if !defined($hdr_edits);
- $hdr_edits->prepend_header('X-Quarantine-Id', '<'.$msginfo->mail_id.'>');
- if ($quarantine_method =~ /^bsmtp:/i) { # X-Envelope-* would be redundant
- } else {
- # NOTE: RFC2821 mentions possible headers X-SMTP-MAIL and X-SMTP-RCPT
- # Exim uses: Envelope-To, Sendmail uses X-Envelope-To;
- # No need with bsmtp or sql, which carry addresses in the envelope
- $hdr_edits->prepend_header('X-Envelope-To',
- join(",\n ", qquote_rfc2821_local(@{$msginfo->recips})), 1);
- $hdr_edits->prepend_header('X-Envelope-From',
- qquote_rfc2821_local($sender));
- }
- do_log(5, "DO_QUARANTINE, sender: " . $quar_msg->sender);
- $quar_msg->auth_submitter(quote_rfc2821_local($quar_msg->sender));
- $quar_msg->auth_user(c('amavis_auth_user'));
- $quar_msg->auth_pass(c('amavis_auth_pass'));
- $quar_msg->header_edits($hdr_edits);
- $quar_msg->mail_text($msginfo->mail_text); # use the same mail contents
- snmp_count('QuarMsgs');
- mail_dispatch($conn, $quar_msg, 1, 0);
- my($n_smtp_resp, $n_exit_code, $n_dsn_needed) =
- one_response_for_all($quar_msg, 0, am_id()); # check status
- if ($n_smtp_resp =~ /^2/ && !$n_dsn_needed) { # ok
- snmp_count($snmp_id eq '' ? 'QuarOther' : $snmp_id);
- } elsif ($n_smtp_resp =~ /^4/) {
- snmp_count('QuarAttemptTempFails');
- die "temporarily unable to quarantine: $n_smtp_resp";
- } else { # abort if quarantining not successful
- snmp_count('QuarAttemptFails');
- die "Can not quarantine: $n_smtp_resp";
- }
- my($quar_type);
- my(@qa); my(%seen); # collect unique quarantine mailboxes or addresses
- my($existing_qa) = $msginfo->quarantined_to;
- if (ref $existing_qa) { @qa = @$existing_qa; $seen{$_}++ for (@qa) }
- for my $r (@{$quar_msg->per_recip_data}) {
- my($mbxname) = $r->recip_mbxname;
- if ($mbxname ne '' && !$seen{$mbxname}++) {
- push(@qa,$mbxname);
- $quar_type = /^bsmtp:/ ? 'B' : /^smtp:/ ? 'M' : /^sql:/ ? 'Q' :
- /^local:/ ? ($mbxname=~/\@/ ? 'M' : $mbxname=~/\.gz\z/ ? 'Z' : 'F')
- : '?' for (lc($quarantine_method));
- }
- }
- $msginfo->quar_type($quar_type);
- $msginfo->quarantined_to(\@qa); # remember where it was quarantined to
- do_log(5, "DO_QUARANTINE done");
- }
- }
- # if virus/banned/bad-header found - quarantine it and send notifications
- sub do_virus($$$) {
- my($conn, $msginfo, $virus_dejavu) = @_;
- my($q_method, $quarantine_to_maps_ref, $admin_maps_ref) =
- @virusname ?
- (c('virus_quarantine_method'),
- ca('virus_quarantine_to_maps'),
- ca('virus_admin_maps') )
- : $banned_filename_any ?
- (c('banned_files_quarantine_method'),
- ca('banned_quarantine_to_maps'),
- ca('banned_admin_maps') )
- : @bad_headers ?
- (c('bad_header_quarantine_method'),
- ca('bad_header_quarantine_to_maps'),
- ca('bad_header_admin_maps') )
- : (undef, undef, undef, undef);
- do_log(5, "do_virus: looking for per-recipient quarantine and admins");
- my($newvirus_admin_maps_ref) =
- @virusname && !$virus_dejavu ? ca('newvirus_admin_maps') : undef;
- my(@q_addr,@a_addr); # get per-recipient quarantine address(es) and admins
- for my $r (@{$msginfo->per_recip_data}) {
- my($rec) = $r->recip_addr;
- my($q); # quarantine (pseudo) address associated with the recipient
- my($a); # administrator's e-mail address
- ($q) = lookup(0,$rec,@$quarantine_to_maps_ref) if $quarantine_to_maps_ref;
- $q = $rec if $q ne '' && $q_method =~ /^bsmtp:/i; # orig.recip when BSMTP
- ($a) = lookup(0,$rec,@$admin_maps_ref) if $admin_maps_ref;
- push(@q_addr, $q) if defined $q && $q ne '' && !grep {$_ eq $q} @q_addr;
- push(@a_addr, $a) if defined $a && $a ne '' && !grep {$_ eq $a} @a_addr;
- if ($newvirus_admin_maps_ref) {
- ($a) = lookup(0,$rec,@$newvirus_admin_maps_ref);
- push(@a_addr, $a) if defined $a && $a ne '' && !grep {$_ eq $a} @a_addr;
- }
- }
- if (@q_addr) { # do the quarantining
- # prepare header edits for the quarantined message
- my($hdr_edits) = Amavis::Out::EditHeader->new;
- if (@virusname) {
- $hdr_edits->append_header('X-Amavis-Alert',
- "INFECTED, message contains virus:\n " . join(",\n ", @virusname), 1);
- }
- for my $r (@{$msginfo->per_recip_data}) {
- my(@b); @b = @{$r->banned_parts} if defined $r->banned_parts;
- if (@b) {
- my($b_chopped) = @b > 3; @b = @b[0..2] if $b_chopped;
- my($msg) = "BANNED, message contains " . (@b==1 ? 'part' : 'parts') .
- ":\n " . join(",\n ", @b) . ($b_chopped ? ", ..." : "");
- $msg =~ s/[ \t]{6,}/ ... /g;
- $hdr_edits->append_header('X-Amavis-Alert', $msg, 1);
- last; # ***fudge: only the first recipient's banned hit will be shown
- }
- }
- if (@bad_headers) {
- $hdr_edits->append_header('X-Amavis-Alert',
- 'BAD HEADER '.$bad_headers[0], 1);
- }
- do_quarantine($conn,$msginfo,$hdr_edits,\@q_addr,$q_method,
- @virusname ? 'QuarVirusMsgs' :
- $banned_filename_any ? 'QuarBannedMsgs' :
- @bad_headers ? 'QuarBadHMsgs' : 'QuarOther');
- }
- my($hdr_edits) = Amavis::Out::EditHeader->new;
- if (!@a_addr) {
- do_log(4, "Skip admin notification, no administrators");
- } else { # notify per-recipient virus administrators
- ll(5) && do_log(5, sprintf("DO_VIRUS - NOTIFICATIONS to %s; sender: %s",
- join(",",qquote_rfc2821_local(@a_addr)), $msginfo->sender));
- my($notification) = Amavis::In::Message->new;
- $notification->rx_time($msginfo->rx_time); # copy the reception time
- $notification->delivery_method(c('notify_method'));
- $notification->sender(c('mailfrom_notify_admin'));
- $notification->auth_submitter(
- quote_rfc2821_local(c('mailfrom_notify_admin')));
- $notification->auth_user(c('amavis_auth_user'));
- $notification->auth_pass(c('amavis_auth_pass'));
- $notification->recips([@a_addr]);
- my(%mybuiltins) = %builtins; # make a local copy
- $mybuiltins{'T'} = \@a_addr; # used in 'To:'
- $mybuiltins{'f'} = c('hdrfrom_notify_admin'); # From:
- $notification->mail_text(
- string_to_mime_entity(expand(cr('notify_virus_admin_templ'),
- \%mybuiltins)));
- # $notification->body_type('7BIT');
- $notification->header_edits($hdr_edits);
- mail_dispatch($conn, $notification, 1, 0);
- my($n_smtp_resp, $n_exit_code, $n_dsn_needed) =
- one_response_for_all($notification, 0, am_id()); # check status
- if ($n_smtp_resp =~ /^2/ && !$n_dsn_needed) { # ok
- } elsif ($n_smtp_resp =~ /^4/) {
- die "temporarily unable to notify virus admin: $n_smtp_resp";
- } else {
- do_log(-1, "FAILED to notify virus admin: $n_smtp_resp");
- }
- # $notification->purge;
- }
- for my $r (@{$msginfo->per_recip_data}) {
- my($wr) = 0; my($rec) = $r->recip_addr;
- if (!c('warn_offsite') && !lookup(0,$rec,@{ca('local_domains_maps')})) {
- # not notifying foreign recipients
- # } elsif (! defined($msginfo->sender_contact) ) { # (not general enough)
- # do_log(5,"do_virus: skip recip notifications for unknown sender");
- } elsif ($r->infected) {
- $wr = lookup(0,$rec,@{ca('warnvirusrecip_maps')});
- } elsif (defined($r->banned_parts) && @{$r->banned_parts}) {
- $wr = lookup(0,$rec,@{ca('warnbannedrecip_maps')});
- } elsif (@bad_headers &&
- !lookup(0,$rec,@{ca('bypass_header_checks_maps')})) {
- $wr = lookup(0,$rec,@{ca('warnbadhrecip_maps')});
- }
- if ($wr) { # warn recipient
- my($notification) = Amavis::In::Message->new;
- $notification->rx_time($msginfo->rx_time); # copy the reception time
- $notification->delivery_method(c('notify_method'));
- $notification->sender(c('mailfrom_notify_recip'));
- $notification->auth_submitter(
- quote_rfc2821_local(c('mailfrom_notify_recip')));
- $notification->auth_user(c('amavis_auth_user'));
- $notification->auth_pass(c('amavis_auth_pass'));
- $notification->recips([$rec]);
- my(@b); @b = @{$r->banned_parts} if defined $r->banned_parts;
- my($b_chopped) = @b > 2; @b = (@b[0,1],'...') if $b_chopped;
- s/[ \t]{6,}/ ... /g for @b;
- my(%mybuiltins) = %builtins; # make a local copy
- $mybuiltins{'F'} = \@b; # list of banned file names
- $mybuiltins{'f'} = c('hdrfrom_notify_recip'); # 'From:'
- $mybuiltins{'T'} = quote_rfc2821_local($rec); # 'To:'
- my $foo = expand(cr('notify_virus_recips_templ'), \%mybuiltins);
- my $bar = cr('notify_virus_recips_templ');
- warn "++++++++++ . ". $$foo . "+++++++++";
- warn "-----------" . $$bar . "------";
- $notification->mail_text(
- string_to_mime_entity(expand(cr('notify_virus_recips_templ'),
- \%mybuiltins)) );
- # $notification->body_type('7BIT');
- $notification->header_edits($hdr_edits);
- mail_dispatch($conn, $notification, 1, 0);
- my($n_smtp_resp, $n_exit_code, $n_dsn_needed) =
- one_response_for_all($notification, 0, am_id()); # check status
- if ($n_smtp_resp =~ /^2/ && !$n_dsn_needed) { # ok
- } elsif ($n_smtp_resp =~ /^4/) {
- die "temporarily unable to notify recipient rec: $n_smtp_resp";
- } else {
- do_log(-1, "FAILED to notify recipient $rec: $n_smtp_resp");
- }
- # $notification->purge;
- }
- }
- do_log(5, "DO_VIRUS - DONE");
- }
- #
- # if spam found - quarantine it and log report
- sub do_spam($$$$$$) {
- my($conn, $msginfo,
- $spam_level, $spam_status, $spam_report, $autolearn_status) = @_;
- my($q_method) = c('spam_quarantine_method');
- # use the smallest value as the level reported in quarantined headers!
- my($tag_level) =
- min(map { scalar(lookup(0,$_,@{ca('spam_tag_level_maps')})) } @{$msginfo->recips});
- my($tag2_level) =
- min(map { scalar(lookup(0,$_,@{ca('spam_tag2_level_maps')})) } @{$msginfo->recips});
- my($kill_level) =
- min(map { scalar(lookup(0,$_,@{ca('spam_kill_level_maps')})) } @{$msginfo->recips});
- my($blacklisted) =
- scalar(grep { $_->recip_blacklisted_sender } @{$msginfo->per_recip_data});
- my($whitelisted) =
- scalar(grep { $_->recip_whitelisted_sender } @{$msginfo->per_recip_data});
- my($s) = $spam_status; $s =~ s/,/,\n /g; # allow header field wrapping
- my(@boost) = map { $_->recip_score_boost } @{$msginfo->per_recip_data};
- my($full_spam_status) = sprintf(
- "%s,\n score=%s\n tag=%s\n tag2=%s\n kill=%s\n %s%s",
- (defined $spam_level && defined $tag2_level && $spam_level>=$tag2_level ?
- 'Yes' : 'No'),
- (map { !defined $_ ? 'x' : 0+sprintf("%.3f",$_) }
- ($spam_level+max(@boost), $tag_level, $tag2_level, $kill_level)),
- join('', $blacklisted ? "BLACKLISTED\n " : (),
- $whitelisted ? "WHITELISTED\n " : ()),
- $s);
- do_log(5, "do_spam: looking for a quarantine address");
- my(@q_addr,@a_addr); # quarantine address(es) and administrators
- my($sqbsm) = ca('spam_quarantine_bysender_to_maps');
- if (@$sqbsm) { # by-sender quarantine
- my($q); $q = lookup(0,$msginfo->sender, @$sqbsm);
- push(@q_addr, $q) if defined $q && $q ne '' && !grep {$_ eq $q} @q_addr;
- }
- # get per-recipient quarantine address(es) and admins
- for my $r (@{$msginfo->per_recip_data}) {
- my($rec) = $r->recip_addr;
- my($q); # quarantine (pseudo) address associated with the recipient
- ($q) = lookup(0,$rec, @{ca('spam_quarantine_to_maps')});
- if ($q ne '' && defined $spam_level) {
- my($cutoff) = lookup(0,$rec,@{ca('spam_quarantine_cutoff_level_maps')});
- if (!defined $cutoff || $cutoff eq '') {}
- elsif ($spam_level + $r->recip_score_boost >= $cutoff) {
- do_log(2, "do_spam: spam level exceeds quarantine cutoff level $cutoff");
- $q = ''; # disable quarantine on behalf of this recipient
- }
- }
- $q = $rec if $q ne '' && $q_method =~ /^bsmtp:/i; # orig.recip when BSMTP
- my($a) = lookup(0,$rec, @{ca('spam_admin_maps')});
- push(@q_addr, $q) if defined $q && $q ne '' && !grep {$_ eq $q} @q_addr;
- push(@a_addr, $a) if defined $a && $a ne '' && !grep {$_ eq $a} @a_addr;
- }
- if (@q_addr) { # do the quarantining
- # prepare header edits for the quarantined message
- my($hdr_edits) = Amavis::Out::EditHeader->new;
- $hdr_edits->append_header('X-Spam-Status', $full_spam_status, 1);
- $hdr_edits->append_header('X-Spam-Score',
- !defined $spam_level ? '-' : 0+sprintf("%.3f",$spam_level+max(@boost)) );
- my($slc) = c('sa_spam_level_char');
- $hdr_edits->append_header('X-Spam-Level',
- $slc x min(0+$spam_level,64)) if $slc ne '';
- $hdr_edits->append_header('X-Spam-Flag', !$whitelisted &&
- ($blacklisted || (defined $spam_level && defined $tag2_level &&
- $spam_level >= $tag2_level)) ? 'YES' : 'NO');
- $hdr_edits->append_header('X-Spam-Report', $spam_report,1)
- if c('sa_spam_report_header') && $spam_report ne '';
- do_quarantine($conn,$msginfo,$hdr_edits,\@q_addr,$q_method,'QuarSpamMsgs');
- }
- $s = $full_spam_status; $s =~ s/\n[ \t]/ /g;
- ll(2) && do_log(2,sprintf("SPAM, %s -> %s, %s%s%s",
- qquote_rfc2821_local($msginfo->sender_source),
- join(',', qquote_rfc2821_local(@{$msginfo->recips})), $s,
- $autolearn_status eq '' ? '' : ", autolearn=$autolearn_status",
- !@q_addr ? '' : sprintf(", quarantine %s (%s)",
- $msginfo->mail_id, join(',',@q_addr)) ));
- if (!@a_addr) {
- do_log(4, "Skip spam admin notification, no administrators");
- } else { # notify per-recipient spam administrators
- ll(5) && do_log(5, sprintf("DO_SPAM - NOTIFICATIONS to %s; sender: %s",
- join(",",qquote_rfc2821_local(@a_addr)), $msginfo->sender));
- my($notification) = Amavis::In::Message->new;
- $notification->rx_time($msginfo->rx_time); # copy the reception time
- $notification->delivery_method(c('notify_method'));
- $notification->sender(c('mailfrom_notify_spamadmin'));
- $notification->auth_submitter(
- quote_rfc2821_local(c('mailfrom_notify_spamadmin')));
- $notification->auth_user(c('amavis_auth_user'));
- $notification->auth_pass(c('amavis_auth_pass'));
- $notification->recips([@a_addr]);
- my(%mybuiltins) = %builtins; # make a local copy
- $mybuiltins{'T'} = \@a_addr; # used in 'To:'
- $mybuiltins{'f'} = c('hdrfrom_notify_spamadmin');
- $notification->mail_text(
- string_to_mime_entity(expand(cr('notify_spam_admin_templ'),
- \%mybuiltins)));
- # $notification->body_type('7BIT');
- my($hdr_edits) = Amavis::Out::EditHeader->new;
- $notification->header_edits($hdr_edits);
- mail_dispatch($conn, $notification, 1, 0);
- my($n_smtp_resp, $n_exit_code, $n_dsn_needed) =
- one_response_for_all($notification, 0, am_id()); # check status
- if ($n_smtp_resp =~ /^2/ && !$n_dsn_needed) { # ok
- } elsif ($n_smtp_resp =~ /^4/) {
- die "temporarily unable to notify spam admin: $n_smtp_resp";
- } else {
- do_log(-1, "FAILED to notify spam admin: $n_smtp_resp");
- }
- # $notification->purge;
- }
- do_log(5, "DO_SPAM DONE");
- }
- # Calculate message digest;
- # While at it, also get message size, check for 8-bit data, and store original
- # header, since we need it for the %H macro, and MIME::Tools may modify it.
- #
- sub get_body_digest($$) {
- my($fh, $msginfo) = @_;
- $fh->seek(0,0) or die "Can't rewind mail file: $!";
- # choose message digest method:
- my($hctx) = Digest::MD5->new; # 128 bits (32 hex digits)
- my($bctx) = Digest::MD5->new; # 128 bits (32 hex digits)
- # my($bctx) = Digest::SHA1->new; # 160 bits (40 hex digits), slightly slower
- my($h_8bit,$b_8bit) = (0,0);
- my(@orig_header); my($header_size)=0; my($body_size)=0; my($ln);
- for (undef $!; defined($ln=<$fh>); undef $!) { # skip mail header
- last if $ln eq $eol;
- $header_size += length($ln);
- $ln=~/^[\000-\177]*\z/ or $h_8bit=1;
- $hctx->add($ln); push(@orig_header,$ln); # with trailing EOL
- }
- defined $ln || $!==0 or die "Error reading mail header: $!";
- add_entropy($hctx->digest); # faster than traversing @orig_header again
- my($len);
- while (($len = read($fh,$_,16384)) > 0) {
- $bctx->add($_); $body_size += $len;
- /^[\000-\177]*\z/ or $b_8bit=1; # much faster than !/[^\000-\177]/
- }
- defined $len or die "Error reading mail body: $!";
- my($signature) = $bctx->hexdigest;
- # my($signature) = $bctx->b64digest;
- add_entropy($signature);
- $signature = untaint($signature) # checked (either 32 or 40 char)
- if $signature =~ /^ [0-9a-fA-F]{32} (?: [0-9a-fA-F]{8} )? \z/x;
- # store information obtained
- $msginfo->orig_header(\@orig_header);
- $msginfo->orig_header_size($header_size);
- $msginfo->orig_body_size($body_size);
- $msginfo->body_digest($signature);
- # check for 8-bit characters and adjust body type if necessary (rfc1652)
- my($bt_orig) = $msginfo->body_type;
- my($bt_true) = $h_8bit || $b_8bit ? '8BITMIME' : '7BIT';
- if (!defined($bt_orig) || $bt_orig eq '') {
- do_log(4,"setting body type: $bt_true ($h_8bit,$b_8bit)");
- $msginfo->body_type($bt_true);
- } elsif ($bt_true eq '8BITMIME' && uc($bt_orig) ne '8BITMIME') {
- do_log(4,"changing body type: $bt_orig => $bt_true ($h_8bit,$b_8bit)");
- $msginfo->body_type($bt_true);
- }
- do_log(3, "body hash: $signature");
- section_time('body_digest');
- $signature;
- }
- sub find_program_path($$$) {
- my($fv_list, $path_list_ref, $may_log) = @_;
- $fv_list = [$fv_list] if !ref $fv_list;
- my($found);
- for my $fv (@$fv_list) {
- my(@fv_cmd) = split(' ',$fv);
- if (!@fv_cmd) { # empty, not available
- } elsif ($fv_cmd[0] =~ /^\//) { # absolute path
- my($errn) = stat($fv_cmd[0]) ? 0 : 0+$!;
- if ($errn == ENOENT) { }
- elsif ($errn) {
- do_log(-1, "find_program_path: " . "$fv_cmd[0] inaccessible: $!")
- if $may_log;
- } elsif (-x _ && !-d _) { $found = join(' ', @fv_cmd) }
- } elsif ($fv_cmd[0] =~ /\//) { # relative path
- die "find_program_path: relative paths not implemented: @fv_cmd\n";
- } else { # walk through the specified PATH
- for my $p (@$path_list_ref) {
- my($errn) = stat("$p/$fv_cmd[0]") ? 0 : 0+$!;
- if ($errn == ENOENT) { }
- elsif ($errn) {
- do_log(-1, "find_program_path: " . "$p/$fv_cmd[0] inaccessible: $!")
- if $may_log;
- } elsif (-x _ && !-d _) {
- $found = $p . '/' . join(' ', @fv_cmd);
- last;
- }
- }
- }
- last if defined $found;
- }
- $found;
- }
- sub find_external_programs($) {
- my($path_list_ref) = @_;
- for my $f (qw($file $dspam)) {
- my($g) = $f; $g =~ s/\$/Amavis::Conf::/; my($fv_list) = eval('$' . $g);
- my($found) = find_program_path($fv_list, $path_list_ref, 1);
- { no strict 'refs'; $$g = $found } # NOTE: a symbolic reference
- if (!defined $found) { do_log(-1,sprintf("No %-19s not using it", "$f,")) }
- else {
- do_log(0,sprintf("Found %-16s at %s%s", $f,
- $daemon_chroot_dir ne '' ? "(chroot: $daemon_chroot_dir/) " : '',
- $found));
- }
- }
- # map program name path hints to full paths for decoders
- my(%any_st);
- for my $f (@{ca('decoders')}) {
- next if !defined $f || !ref $f; # empty, skip
- my($short_type) = $f->[0]; my(@tried,@found); my($any) = 0;
- for my $d (@$f[2..$#$f]) { # all but the first two elements are programs
- # allow one level of indirection
- my($dd) = (ref $d eq 'SCALAR' || ref $d eq 'REF') ? $$d : $d;
- my($found) = find_program_path($dd, $path_list_ref, 1);
- if (defined $found) { $any++; $dd = $found; $d = $dd; push(@found,$dd) }
- else { push(@tried, !ref($dd) ? $dd : join(", ",@$dd)) if $dd ne '' }
- }
- my($is_a_backup) = $any_st{$short_type};
- my($ll,$tier) = !$is_a_backup ? (0,'') : (2,' (backup, not used)');
- if (@$f <= 2) { # no external programs specified
- do_log($ll, sprintf("Internal decoder for .%-4s%s", $short_type,$tier));
- $f = undef if $is_a_backup; # discard a backup entry
- } elsif (!$any) { # external programs specified but none found
- do_log($ll, sprintf("No decoder for .%-4s%s", $short_type,
- !@tried ? '' : ' tried: '.join("; ",@tried))) if !$is_a_backup;
- $f = undef; # release its storage
- } else {
- do_log($ll, sprintf("Found decoder for .%-4s at %s%s%s", $short_type,
- $daemon_chroot_dir ne '' ? "(chroot: $daemon_chroot_dir/) " : '',
- join("; ",@found), $tier));
- $f = undef if $is_a_backup; # discard a backup entry
- }
- $any_st{$short_type}++ if defined $f;
- }
- # map program name hints to full paths - av scanners
- my($tier) = 'primary'; # primary, secondary, ... av scanners
- for my $f (@{ca('av_scanners')}, "\000", @{ca('av_scanners_backup')}) {
- if ($f eq "\000") { # next tier
- $tier = 'secondary';
- } elsif (!defined $f || !ref $f) { # empty, skip
- } elsif (ref($f->[1]) eq 'CODE') {
- do_log(0, "Using internal av scanner code for ($tier) " . $f->[0]);
- } else {
- my($found) = $f->[1] = find_program_path($f->[1], $path_list_ref, 1);
- if (!defined $found) {
- do_log(3, "No $tier av scanner: " . $f->[0]);
- $f = undef; # release its storage
- } else {
- do_log(0, sprintf("Found $tier av scanner %-11s at %s%s", $f->[0],
- $daemon_chroot_dir ne '' ? "(chroot: $daemon_chroot_dir/) " : '',
- $found));
- }
- }
- }
- }
- # Fetch remaining modules, all must be loaded before chroot and fork occurs
- sub fetch_modules_extra() {
- my(@modules);
- if ($extra_code_sql_base) {
- push(@modules, 'DBI');
- for (@lookup_sql_dsn, @storage_sql_dsn) {
- my(@dsn) = split(/:/,$_->[0],-1);
- push(@modules, 'DBD::'.$dsn[1]) if uc($dsn[0]) eq 'DBI';
- }
- }
- push(@modules, qw(Net::LDAP Net::LDAP::Util Net::LDAP::Search))
- if $extra_code_ldap;
- if (c('bypass_decode_parts') &&
- !grep {exists $policy_bank{$_}{'bypass_decode_parts'} &&
- !$policy_bank{$_}{'bypass_decode_parts'} } keys %policy_bank) {
- } else {
- push(@modules, qw(Convert::TNEF Convert::UUlib Archive::Zip Archive::Tar));
- }
- push(@modules, 'Mail::SpamAssassin') if $extra_code_antispam;
- push(@modules, 'Authen::SASL') if c('auth_required_out');
- Amavis::Boot::fetch_modules('REQUIRED ADDITIONAL MODULES', 1, @modules);
- my($sa_version);
- $sa_version = Mail::SpamAssassin::Version() if $extra_code_antispam;
- @modules = (); # now start collecting optional modules
- if ($unicode_aware) {
- push(@modules, qw(
- bytes bytes_heavy.pl utf8 utf8_heavy.pl
- Encode Encode::Byte Encode::MIME::Header Encode::Unicode::UTF7
- Encode::CN Encode::TW Encode::KR Encode::JP
- unicore::Canonical.pl unicore::Exact.pl unicore::PVA.pl
- unicore::To::Fold.pl unicore::To::Title.pl
- unicore::To::Lower.pl unicore::To::Upper.pl
- ));
- }
- if ($extra_code_antispam) {
- push(@modules, qw(
- Mail::SpamAssassin::Locker::Flock
- Mail::SpamAssassin::Locker::UnixNFSSafe
- Mail::SpamAssassin::DBBasedAddrList
- Mail::SpamAssassin::SQLBasedAddrList
- Mail::SpamAssassin::PersistentAddrList
- Mail::SpamAssassin::PerMsgLearner
- Mail::SpamAssassin::AutoWhitelist
- Mail::SpamAssassin::BayesStore::DBM
- Mail::SpamAssassin::BayesStore::SQL
- Mail::SpamAssassin::Plugin::Hashcash
- Mail::SpamAssassin::Plugin::RelayCountry
- Mail::SpamAssassin::Plugin::SPF
- Mail::SpamAssassin::Plugin::URIDNSBL
- DBD::mysql Sys::Hostname::Long
- Mail::SPF::Query Razor2::Client::Agent Net::CIDR::Lite
- Net::DNS::RR::SOA Net::DNS::RR::NS Net::DNS::RR::MX
- Net::DNS::RR::A Net::DNS::RR::AAAA Net::DNS::RR::PTR
- Net::DNS::RR::CNAME Net::DNS::RR::TXT Net::Ping
- ));
- # ??? ArchiveIterator Reporter Data::Dumper Getopt::Long Sys::Syslog lib
- # Mail::SpamAssassin::BayesStore::SDBM
- }
- if ($extra_code_antispam && defined $sa_version) {
- # *** note that $sa_version could be 3.0.1, which is not really numeric!
- if ($sa_version=~/^(\d+(?:\.\d+)?)/ && $1 < 3) { push(@modules, qw(
- Mail::SpamAssassin::UnixLocker Mail::SpamAssassin::BayesStoreDBM
- Mail::SpamAssassin::SpamCopURI
- URI URI::Escape URI::Heuristic URI::QueryParam URI::Split URI::URL
- URI::WithBase URI::_foreign URI::_generic URI::_ldap URI::_login
- URI::_query URI::_segment URI::_server URI::_userpass URI::data URI::ftp
- URI::gopher URI::http URI::https URI::ldap URI::ldapi URI::ldaps
- URI::mailto URI::mms URI::news URI::nntp URI::pop URI::rlogin URI::rsync
- URI::rtsp URI::rtspu URI::sip URI::sips URI::snews URI::ssh URI::telnet
- URI::tn3270 URI::urn URI::urn::isbn URI::urn::oid
- URI::file URI::file::Base URI::file::Unix URI::file::Win32
- ));
- } elsif ($sa_version=~/^(\d+(?:\.\d+)?)/ && $1 >= 3.1) { push(@modules, qw(
- Mail::SpamAssassin::BayesStore::MySQL
- Mail::SpamAssassin::Plugin::AutoLearnThreshold
- Mail::SpamAssassin::Plugin::ReplaceTags
- Mail::SpamAssassin::Plugin::MIMEHeader
- Mail::SpamAssassin::Plugin::AWL Mail::SpamAssassin::Plugin::DCC
- Mail::SpamAssassin::Plugin::Pyzor Mail::SpamAssassin::Plugin::Razor2
- Mail::SpamAssassin::Plugin::SpamCop
- Mail::SpamAssassin::Plugin::WhiteListSubject
- Mail::SpamAssassin::Plugin::DomainKeys
- Mail::DomainKeys::Header Mail::DomainKeys::Message
- Mail::DomainKeys::Policy Mail::DomainKeys::Signature
- Mail::DomainKeys::Key Mail::DomainKeys::Key::Public
- Crypt::OpenSSL::RSA
- auto::Crypt::OpenSSL::RSA::_new auto::Crypt::OpenSSL::RSA::DESTROY
- auto::Crypt::OpenSSL::RSA::load_public_key
- auto::Crypt::OpenSSL::RSA::new_public_key
- IP::Country::Fast
- ));
- # BayesStore::PgSQL BayesStore::SDBM
- # Plugin::AntiVirus Plugin::DomainKeys Plugin::NetCache Plugin::TextCat
- }
- }
- my($missing);
- $missing = Amavis::Boot::fetch_modules('PRE-COMPILE OPTIONAL MODULES', 0,
- @modules) if @modules;
- do_log(2, 'INFO: no optional modules: '.join(' ',@$missing))
- if ref $missing && @$missing;
- # load optional modules SAVI and Mail::ClamAV if available and requested
- if ($extra_code_antivirus) {
- my($clamav_module_ok);
- for my $entry (@{ca('av_scanners')}, @{ca('av_scanners_backup')}) {
- if (ref($entry) ne 'ARRAY') { # none
- } elsif ($entry->[1] eq \&ask_sophos_savi ||
- $entry->[1] eq \&sophos_savi ||
- $entry->[0] eq 'Sophos SAVI') {
- if (defined(eval { require SAVI }) && SAVI->VERSION(0.30) &&
- Amavis::AV::sophos_savi_init(@$entry)) {} # ok, loaded
- else { $entry->[1] = undef } # disable entry
- } elsif ($entry->[1] eq \&ask_clamav ||
- $entry->[0] =~ /^Mail::ClamAV/) {
- if (!defined($clamav_module_ok)) {
- $clamav_module_ok = eval { require Mail::ClamAV };
- $clamav_module_ok = 0 if !defined $clamav_module_ok;
- }
- $entry->[1] = undef if !$clamav_module_ok; # disable entry
- }
- }
- }
- }
- #
- # Main program starts here
- #
- # Read dynamic source code, and logging and notification message templates
- # from the end of this file (pseudo file handle DATA)
- #
- $Amavis::Conf::notify_spam_admin_templ = ''; # not used
- $Amavis::Conf::notify_spam_recips_templ = ''; # not used
- do { local($/) = "__DATA__\n"; # set line terminator to this string
- chomp($_ = <Amavis::DATA>) for (
- $extra_code_db, $extra_code_cache,
- $extra_code_sql_base, $extra_code_sql_log, $extra_code_sql_quar,
- $extra_code_sql_lookup, $extra_code_ldap,
- $extra_code_in_amcl, $extra_code_in_smtp,
- $extra_code_antivirus, $extra_code_antispam, $extra_code_unpackers,
- $Amavis::Conf::log_templ, $Amavis::Conf::log_recip_templ);
- if ($unicode_aware) {
- # binmode(\*Amavis::DATA, ":encoding(utf8)") # :encoding(iso-8859-1)
- # or die "Can't set \*DATA encoding: $!";
- }
- chomp($_ = <Amavis::DATA>) for (
- $Amavis::Conf::notify_sender_templ,
- $Amavis::Conf::notify_virus_sender_templ,
- $Amavis::Conf::notify_virus_admin_templ,
- $Amavis::Conf::notify_virus_recips_templ,
- $Amavis::Conf::notify_spam_sender_templ,
- $Amavis::Conf::notify_spam_admin_templ );
- }; # restore line terminator
- close(\*Amavis::DATA) or die "Error closing *Amavis::DATA: $!";
- # close(STDIN) or die "Error closing STDIN: $!";
- # note: don't close STDIN just yet to prevent some other file taking up fd 0
- # discard trailing NL
- $Amavis::Conf::log_templ = $1
- if $Amavis::Conf::log_templ=~/^(.*?)[\r\n]+\z/s;
- $Amavis::Conf::log_recip_templ = $1
- if $Amavis::Conf::log_recip_templ=~/^(.*?)[\r\n]+\z/s;
- # Consider droping privileges early, before reading config file.
- # This is only possible if running under chroot will not be needed.
- #
- my($desired_group); # defaults to $desired_user's group
- my($desired_user); # username or UID
- if ($> != 0) { $desired_user = $> } # use effective UID if not root
- #else {
- # for my $u ('amavis', 'vscan') { # try to guess a good default username
- # my($username,$passwd,$uid,$gid) = getpwnam($u);
- # if (defined $uid && $uid != 0) { $desired_user = $u; last }
- # }
- #}
- # collect and parse command line options
- while (@ARGV >= 2 && $ARGV[0] =~ /^-[ugc]\z/) {
- my($opt) = shift @ARGV;
- if ($opt eq '-u') { # -u username
- my($val) = shift @ARGV;
- if ($> == 0) { $desired_user = $val }
- else { print STDERR "Ignoring option -u when not running as root\n" }
- } elsif ($opt eq '-g') { # -g group
- my($val) = shift @ARGV;
- if ($> == 0) { $desired_group = $val }
- else { print STDERR "Ignoring option -g when not running as root\n" }
- } elsif ($opt eq '-c') { # -c config_file
- push(@config_files, untaint(shift @ARGV));
- }
- }
- if (defined $desired_user && ($> == 0 || $< == 0)) { # drop privileges early
- my($username,$passwd,$uid,$gid) =
- $desired_user=~/^(\d+)$/ ? (undef,undef,$1,undef) :getpwnam($desired_user);
- defined $uid or die "No such username: $desired_user\n";
- if ($desired_group eq '') { $desired_group = $gid } # for logging purposes
- else { $gid = $desired_group=~/^(\d+)$/ ? $1 : getgrnam($desired_group) }
- defined $gid or die "No such group: $desired_group\n";
- $( = $gid; # real GID
- $) = "$gid $gid"; # effective GID
- POSIX::setuid($uid) or die "Can't setuid to $uid: $!";
- $> = $uid; $< = $uid; # just in case
- # print STDERR "desired user=$desired_user ($uid), current: EUID: $> ($<)\n";
- # print STDERR "desired group=$desired_group, current: EGID: $) ($()\n";
- $> != 0 or die "Still running as root, aborting\n";
- $< != 0 or die "Effective UID changed, but Real UID is 0\n";
- }
- umask(0027);
- POSIX::setlocale(LC_TIME,"C"); # English dates required in syslog and rfc2822!
- # do some remaining initialization
- init_builtin_macros();
- init_local_delivery_aliases();
- Amavis::Conf::init_decoders();
- Amavis::Conf::build_default_maps();
- # default location of the config file if none specified
- push(@config_files, '/etc/amavisd.conf') if !@config_files;
- # Read/execute the config file, which may override default settings
- Amavis::Conf::read_config(@config_files);
- if (defined $desired_user && $daemon_user ne '') {
- # compare the config file settings to current UID
- my($username,$passwd,$uid,$gid) =
- $daemon_user=~/^(\d+)$/ ? (undef,undef,$1,undef) : getpwnam($daemon_user);
- $uid == $> or warn sprintf(
- "WARN: running under user '%s' (UID=%s), the config file".
- " specifies \$daemon_user='%s' (UID=%s)\n",
- $desired_user, $>, $daemon_user, defined $uid ? $uid : '?');
- }
- # compile optional modules if needed
- # %modules_basic = %INC; # helps to track missing modules in chroot
- if (!$enable_db) { $extra_code_db = undef }
- else {
- eval $extra_code_db or die "Problem in Amavis::DB or Amavis::DB::SNMP code: $@";
- $extra_code_db = 1; # release memory occupied by the source code
- }
- if (!$enable_global_cache || !$extra_code_db) { $extra_code_cache = undef }
- else {
- eval $extra_code_cache or die "Problem in the Amavis::Cache code: $@";
- $extra_code_cache = 1; # release memory occupied by the source code
- }
- if (!@storage_sql_dsn) { $extra_code_sql_log = undef }
- if (!@lookup_sql_dsn) { $extra_code_sql_lookup = undef }
- if (!defined($extra_code_sql_log) || # sql quarantine depends on sql log
- !grep { c($_)=~/^sql:/i } qw(virus_quarantine_method spam_quarantine_method
- banned_files_quarantine_method bad_header_quarantine_method)
- ) { $extra_code_sql_quar = undef }
- if (!defined($extra_code_sql_log) && !defined($extra_code_sql_quar) &&
- !defined($extra_code_sql_lookup)) { $extra_code_sql_base = undef }
- else {
- eval $extra_code_sql_base or die "Problem in Amavis SQL base code: $@";
- $extra_code_sql_base = 1; # release memory occupied by the source code
- }
- if (defined $extra_code_sql_log) {
- eval $extra_code_sql_log or die "Problem in Amavis::SQL::Log code: $@";
- $extra_code_sql_log = 1; # release memory occupied by the source code
- }
- if (defined $extra_code_sql_quar) {
- eval $extra_code_sql_quar or die "Problem in Amavis::SQL::Quarantine code: $@";
- $extra_code_sql_quar = 1; # release memory occupied by the source code
- }
- if (defined $extra_code_sql_lookup) {
- eval $extra_code_sql_lookup or die "Problem in Amavis SQL lookup code: $@";
- $extra_code_sql_lookup = 1; # release memory occupied by the source code
- }
- if (!$enable_ldap) { $extra_code_ldap = undef }
- else {
- eval $extra_code_ldap or die "Problem in the Lookup::LDAP code: $@";
- $extra_code_ldap = 1; # release memory occupied by the source code
- }
- { my(%needed_protocols);
- for my $bank_name (keys %policy_bank) {
- my($var) = $policy_bank{$bank_name}{'protocol'};
- $var = $$var if ref($var) eq 'SCALAR'; # allow one level of indirection
- $needed_protocols{$var} = 1 if defined $var;
- }
- # compatibility with older config files unaware of $protocol config variable
- $needed_protocols{'AM.CL'} = 1
- if defined $unix_socketname && $unix_socketname ne ''
- && !grep {$needed_protocols{$_}} qw(AM.PDP COURIER);
- $needed_protocols{'SMTP'} = 1
- if defined $inet_socket_port && $inet_socket_port ne ''
- && (!ref $inet_socket_port || @$inet_socket_port)
- && !grep {$needed_protocols{$_}} qw(SMTP LMTP QMQPqq);
- if ($needed_protocols{'COURIER'}) { die "In::Courier code not available" }
- if ($needed_protocols{'QMQPqq'}) { die "In::QMQPqq code not available" }
- if ($needed_protocols{'AM.PDP'} || $needed_protocols{'AM.CL'}) {
- eval $extra_code_in_amcl or die "Problem in the In::AMCL code: $@";
- $extra_code_in_amcl = 1; # release memory occupied by the source code
- } else {
- $extra_code_in_amcl = undef;
- }
- if ($needed_protocols{'SMTP'} || $needed_protocols{'LMTP'}) {
- eval $extra_code_in_smtp or die "Problem in the In::SMTP code: $@";
- $extra_code_in_smtp = 1; # release memory occupied by the source code
- } else {
- $extra_code_in_smtp = undef;
- }
- }
- my($bpvcm) = ca('bypass_virus_checks_maps');
- if (!@{ca('av_scanners')} && !@{ca('av_scanners_backup')}) {
- $extra_code_antivirus = undef;
- } elsif (@$bpvcm && !ref($bpvcm->[0]) && $bpvcm->[0]) {
- # do a simple-minded test to make it easy to turn off virus checks
- $extra_code_antivirus = undef;
- } else {
- eval $extra_code_antivirus or die "Problem in the antivirus code: $@";
- $extra_code_antivirus = 1; # release memory occupied by the source code
- }
- if (!$extra_code_antivirus) # release storage
- { @Amavis::Conf::av_scanners = @Amavis::Conf::av_scanners_backup = () }
- my($bpscm) = ca('bypass_spam_checks_maps');
- if (@$bpscm && !ref($bpscm->[0]) && $bpscm->[0]) {
- # do a simple-minded test to make it easy to turn off spam checks
- $extra_code_antispam = undef;
- } else {
- eval $extra_code_antispam or die "Problem in the antispam code: $@";
- $extra_code_antispam = 1; # release memory occupied by the source code
- }
- if (c('bypass_decode_parts') &&
- !grep {exists $policy_bank{$_}{'bypass_decode_parts'} &&
- !$policy_bank{$_}{'bypass_decode_parts'} } keys %policy_bank) {
- $extra_code_unpackers = undef;
- } else {
- eval $extra_code_unpackers or die "Problem in the Amavis::Unpackers code: $@";
- $extra_code_unpackers = 1; # release memory occupied by the source code
- }
- # act on command line parameters
- my($cmd) = lc($ARGV[0]);
- if ($cmd =~ /^(start|debug|debug-sa|foreground)?\z/) {
- $DEBUG=1 if $cmd eq 'debug';
- $daemonize=0 if $cmd eq 'foreground';
- $daemonize=0, $sa_debug='1,all' if $cmd eq 'debug-sa';
- } elsif ($cmd !~ /^(reload|stop)\z/) {
- die "$myversion: Unknown argument. Usage:\n $0 [-u user] [-g group] [-c config-file] ( [start] | stop | reload | debug | debug-sa | foreground )\n";
- } else { # stop or reload
- eval { # first stop a running daemon
- $pid_file ne '' or die "Config parameter \$pid_file not defined";
- my($errn) = stat($pid_file) ? 0 : 0+$!;
- $errn != ENOENT or die "No PID file $pid_file\n";
- $errn == 0 or die "PID file $pid_file inaccessible: $!";
- my($amavisd_pid); local(*PID_FILE); my($ln);
- open(PID_FILE, "< $pid_file\0") or die "Can't open file $pid_file: $!";
- for (undef $!; defined($ln=<PID_FILE>); undef $!)
- { chomp($ln); $amavisd_pid = $ln if $ln =~ /^\d+\z/ }
- defined $ln || $!==0 or die "Error reading from $pid_file: $!";
- close(PID_FILE) or die "Error closing file $pid_file: $!";
- defined($amavisd_pid) or die "Invalid PID in the $pid_file";
- $amavisd_pid = untaint($amavisd_pid);
- kill('TERM',$amavisd_pid) or die "Can't SIGTERM amavisd[$amavisd_pid]: $!";
- my($waited) = 0; my($sigkill_sent) = 0; my($delay) = 1; # seconds
- for (;;) { # wait for the old running daemon to go away
- sleep($delay); $waited += $delay; $delay = 5;
- last if !kill(0,$amavisd_pid); # is the old daemon still there?
- if ($waited < 60 || $sigkill_sent) {
- print STDERR "Waiting for the process $amavisd_pid to terminate\n";
- } else { # use stronger hammer
- print STDERR "Sending SIGKILL to amavisd[$amavisd_pid]\n";
- kill('KILL',$amavisd_pid)
- or warn "Can't SIGKILL amavisd[$amavisd_pid]: $!";
- $sigkill_sent = 1;
- }
- }
- };
- if ($@ ne '') { chomp($@); die "$@, can't $cmd the process\n" }
- exit 0 if $cmd eq 'stop';
- print STDERR "daemon terminated, waiting for the dust to settle...\n";
- sleep 5; # wait for the TCP socket to be released
- print STDERR "becoming a new daemon...\n";
- }
- $daemonize = 0 if $DEBUG;
- # Set path, home and term explictly. Don't trust environment
- $ENV{PATH} = $path if $path ne '';
- $ENV{HOME} = $helpers_home if $helpers_home ne '';
- $ENV{TERM} = 'dumb'; $ENV{COLUMNS} = '80'; $ENV{LINES} = '100';
- Amavis::Log::init($DEBUG, $DO_SYSLOG, $SYSLOG_LEVEL, $LOGFILE);
- # report version of Perl and process UID
- do_log(1, "user=$desired_user, EUID: $> ($<); group=$desired_group, EGID: $) ($()");
- do_log(0, "Perl version $]");
- # insist on a FQDN in $myhostname
- $myhostname =~ /[^.]\.[a-zA-Z0-9]+\z/s || lc($myhostname) eq 'localhost'
- or die <<"EOD";
- The value of variable \$myhostname is \"$myhostname\", but should have been
- a fully qualified domain name; perhaps uname(3) did not provide such.
- You must explicitly assign a FQDN of this host to variable \$myhostname
- in amavisd.conf, or fix what uname(3) provides as a host's network name!
- EOD
- # $SIG{USR2} = sub {
- # my($msg) = Carp::longmess("SIG$_[0] received, backtrace:");
- # print STDERR "\n",$msg,"\n"; do_log(-1,$msg);
- # };
- # pre-parse IP lookup tables to speed up lookups
- for my $bank_name (keys %policy_bank) {
- my($r) = $policy_bank{$bank_name}{'inet_acl'};
- if (ref($r) eq 'ARRAY') # should be a ref to single IP lookup table
- { $policy_bank{$bank_name}{'inet_acl'} = Amavis::Lookup::IP->new(@$r) }
- $r = $policy_bank{$bank_name}{'mynetworks_maps'}; # ref to list of tables
- if (ref($r) eq 'ARRAY') { # should be an array, test just to make sure
- for my $table (@$r) # replace plain lists with Amavis::Lookup::IP objects
- { $table = Amavis::Lookup::IP->new(@$table) if ref($table) eq 'ARRAY' }
- }
- }
- fetch_modules_extra(); # bring additional modules into memory and compile them
- # set up Net::Server configuration
- my $server = bless {
- server => {
- # command args to be used after HUP must be untainted, deflt: [$0,@ARGV]
- # commandline => ['/usr/local/sbin/amavisd','-c',$config_file[0] ],
- commandline => [], # disable
- # listen on the following sockets (one or more):
- port => [ (!defined($unix_socketname) || $unix_socketname eq '' ? ()
- : "$unix_socketname|unix"), # helper
- map { "$_/tcp" } # accept SMTP on this port(s)
- (ref $inet_socket_port ? @$inet_socket_port
- : $inet_socket_port ne '' ? $inet_socket_port : () ),
- ],
- # limit socket bind (e.g. to the loopback interface)
- host => (!defined($inet_socket_bind) || $inet_socket_bind eq '' ? '*'
- : $inet_socket_bind),
- max_servers => $max_servers, # number of pre-forked children
- max_requests => $max_requests, # restart child after that many accept's
- user => (($> == 0 || $< == 0) ? $daemon_user : undef),
- group => (($> == 0 || $< == 0) ? $daemon_group : undef),
- pid_file => $pid_file,
- lock_file => $lock_file, # serialization lockfile
- # serialize => 'flock', # flock, semaphore, pipe
- background => $daemonize ? 1 : undef,
- setsid => $daemonize ? 1 : undef,
- chroot => $daemon_chroot_dir ne '' ? $daemon_chroot_dir : undef,
- no_close_by_child => 1,
- # controls log level for Net::Server internal log messages:
- # 0=err, 1=warning, 2=notice, 3=info, 4=debug
- log_level => ($DEBUG ? 4 : 2),
- log_file => undef, # will be overridden to call do_log()
- },
- }, 'Amavis';
- $0 = 'amavisd (master)';
- $server->run; # transfer control to Net::Server
- # shouldn't get here
- exit 1;
- # we read text (especially notification templates) from DATA sections
- # to avoid any interpretations of special characters (e.g. \ or ') by Perl
- #
- __DATA__
- #
- package Amavis::DB::SNMP;
- use strict;
- use re 'taint';
- BEGIN {
- import Amavis::Conf qw($myversion $myhostname);
- import Amavis::Util qw(ll do_log snmp_counters_get
- add_entropy fetch_entropy);
- }
- use BerkeleyDB;
- BEGIN {
- use Exporter ();
- use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.043';
- @ISA = qw(Exporter);
- }
- # open existing databases (called by each child process)
- sub new {
- my($class,$db_env) = @_; undef $!; my($env) = $db_env->get_db_env;
- defined $env or die "BDB bad db env.: $BerkeleyDB::Error, $!.";
- undef $!; my($dbs) = BerkeleyDB::Hash->new(-Filename=>'snmp.db', -Env=>$env);
- defined $dbs or die "BDB no dbS: $BerkeleyDB::Error, $!.";
- undef $!; my($dbn) = BerkeleyDB::Hash->new(-Filename=>'nanny.db',-Env=>$env);
- defined $dbn or die "BDB no dbN: $BerkeleyDB::Error, $!.";
- bless { 'db_snmp'=>$dbs, 'db_nanny'=>$dbn }, $class;
- }
- sub DESTROY {
- my($self) = shift;
- eval { do_log(5,"Amavis::DB::SNMP DESTROY called") };
- for my $db ($self->{'db_snmp'}, $self->{'db_nanny'}) {
- if (defined $db) {
- eval { $db->db_close==0 or die "db_close: $BerkeleyDB::Error, $!." };
- if ($@ ne '') { warn "BDB S+N DESTROY $@" }
- $db = undef;
- }
- }
- }
- #sub lock_stat($) {
- # my($label) = @_;
- # 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})}\'';
- # do_log(0, "lock_stat $label: $s");
- #}
- # insert startup time SNMP entry, called from the master process at startup
- # (a classical subroutine, not a method)
- sub put_initial_snmp_data($) {
- my($db) = @_;
- my($cursor) = $db->db_cursor(DB_WRITECURSOR);
- defined $cursor or die "BDB S db_cursor: $BerkeleyDB::Error, $!.";
- for my $obj (['sysDescr', 'STR', $myversion],
- ['sysObjectID', 'OID', '1.3.6.1.4.1.15312.2.1'],
- # iso.org.dod.internet.private.enterprise.ijs.amavisd-new.snmp
- ['sysUpTime', 'INT', int(time)],
- # later it must be converted to timeticks (10ms since start)
- ['sysContact', 'STR', ''],
- ['sysName', 'STR', $myhostname],
- ['sysLocation', 'STR', ''],
- ['sysServices', 'INT', 64], # application
- ) {
- my($key,$type,$val) = @$obj;
- $cursor->c_put($key, sprintf("%s %s",$type,$val), DB_KEYLAST) == 0
- or die "BDB S c_put: $BerkeleyDB::Error, $!.";
- };
- $cursor->c_close==0 or die "BDB S c_close: $BerkeleyDB::Error, $!.";
- }
- sub update_snmp_variables {
- my($self) = @_;
- do_log(5,"updating snmp variables");
- my($snmp_var_names_ref) = snmp_counters_get();
- my($eval_stat,$interrupt); $interrupt = '';
- if (defined $snmp_var_names_ref && @$snmp_var_names_ref) {
- my($db) = $self->{'db_snmp'}; my($cursor);
- my($h1) = sub { $interrupt = $_[0] };
- local(@SIG{qw(INT HUP TERM TSTP QUIT ALRM USR1 USR2)}) = ($h1) x 8;
- eval { # ensure cursor will be unlocked even in case of errors or signals
- $cursor = $db->db_cursor(DB_WRITECURSOR); # obtain write lock
- defined $cursor or die "db_cursor: $BerkeleyDB::Error, $!.";
- for my $key (@$snmp_var_names_ref) {
- my($snmp_var_name,$arg,$type) = ref $key ? @$key : ($key);
- $type = 'C32' if !defined($type) || $type eq '';
- $arg = 1 if !defined($arg) && $type eq 'C32';
- my($val,$flags);
- my($stat) = $cursor->c_get($snmp_var_name,$val,DB_SET);
- if ($stat==0) { # exists, update it
- if ($type eq 'C32' && $val=~/^C32 (\d+)\z/) { $val = $1+$arg }
- elsif ($type eq 'INT' && $val=~/^INT (\d+)\z/) { $val = $arg }
- elsif ($type=~/^(STR|OID)\z/ && $val=~/^\Q$type\E (.*)\z/) {
- if ($snmp_var_name ne 'entropy') { $val = $arg }
- else { # blend-in entropy
- $val = $1; add_entropy($val);
- $val = substr(fetch_entropy(),-10,10); # save only 60 tail bits
- }
- }
- else { do_log(-2,"WARN: variable syntax? $val, clearing"); $val = 0 }
- $flags = DB_CURRENT;
- } else { # create new entry
- $stat==DB_NOTFOUND or die "c_get: $BerkeleyDB::Error, $!.";
- $flags = DB_KEYLAST; $val = $arg;
- }
- my($str) = $type =~ /^(C32|INT)\z/ ? sprintf("%010d",$val) : $val;
- $cursor->c_put($snmp_var_name, "$type $str", $flags) == 0
- or die "c_put: $BerkeleyDB::Error, $!.";
- }
- $cursor->c_close==0 or die "c_close: $BerkeleyDB::Error, $!.";
- $cursor = undef;
- };
- $eval_stat = $@;
- if (defined $db) {
- $cursor->c_close if defined $cursor; # unlock, ignoring status
- $cursor = undef;
- # if ($eval_stat eq '') {
- # my($stat); $db->db_sync(); # not really needed
- # $stat==0 or warn "BDB S db_sync, status $stat: $BerkeleyDB::Error, $!.";
- # }
- }
- }
- delete $self->{'cnt'};
- if ($interrupt ne '') { kill($interrupt,$$) } # resignal
- elsif ($eval_stat ne '')
- { chomp($eval_stat); die "update_snmp_variables: BDB S $eval_stat\n" }
- }
- sub read_snmp_variables {
- my($self,@snmp_var_names) = @_;
- my($eval_stat,$interrupt); $interrupt = '';
- my($db) = $self->{'db_snmp'}; my($cursor); my(@values);
- my($h1) = sub { $interrupt = $_[0] };
- local(@SIG{qw(INT HUP TERM TSTP QUIT ALRM USR1 USR2)}) = ($h1) x 8;
- eval { # ensure cursor will be unlocked even in case of errors or signals
- $cursor = $db->db_cursor; # obtain read lock
- defined $cursor or die "db_cursor: $BerkeleyDB::Error, $!.";
- for my $cname (@snmp_var_names) {
- my($val); my($stat) = $cursor->c_get($cname,$val,DB_SET);
- push(@values, $stat==0 ? $val : undef);
- $stat==0 || $stat==DB_NOTFOUND or die "c_get: $BerkeleyDB::Error, $!.";
- }
- $cursor->c_close==0 or die "c_close: $BerkeleyDB::Error, $!.";
- $cursor = undef;
- };
- $eval_stat = $@;
- if (defined $db) {
- $cursor->c_close if defined $cursor; # unlock, ignoring status
- $cursor = undef;
- }
- if ($interrupt ne '') { kill($interrupt,$$) } # resignal
- elsif ($eval_stat ne '')
- { chomp($eval_stat); die "read_snmp_variables: BDB S $eval_stat\n" }
- for my $val (@values) {
- if (!defined($val)) {} # keep undefined
- elsif ($val =~ /^(?:C32|INT) (\d+)\z/) { $val = 0+$1 }
- elsif ($val =~ /^(?:STR|OID) (.*)\z/) { $val = $1 }
- else { do_log(-2,"WARN: counter syntax? $val"); $val = undef }
- }
- \@values;
- }
- sub register_proc {
- my($self,$task_id) = @_;
- my($db) = $self->{'db_nanny'}; my($cursor);
- my($val,$new_val); my($key) = sprintf("%05d",$$);
- $new_val = sprintf("%010d %-12s", time, $task_id) if defined $task_id;
- my($eval_stat,$interrupt); $interrupt = '';
- my($h1) = sub { $interrupt = $_[0] };
- local(@SIG{qw(INT HUP TERM TSTP QUIT ALRM USR1 USR2)}) = ($h1) x 8;
- eval { # ensure cursor will be unlocked even in case of errors or signals
- $cursor = $db->db_cursor(DB_WRITECURSOR); # obtain write lock
- defined $cursor or die "db_cursor: $BerkeleyDB::Error, $!.";
- my($stat) = $cursor->c_get($key,$val,DB_SET);
- $stat==0 || $stat==DB_NOTFOUND or die "c_get: $BerkeleyDB::Error, $!.";
- if ($stat==0 && !defined $task_id) { # remove existing entry
- $cursor->c_del==0 or die "c_del: $BerkeleyDB::Error, $!.";
- } elsif (defined $task_id && !($stat==0 && $new_val eq $val)) {
- # add new, or update existing entry if different
- $cursor->c_put($key, $new_val,
- $stat==0 ? DB_CURRENT : DB_KEYLAST ) == 0
- or die "c_put: $BerkeleyDB::Error, $!.";
- }
- $cursor->c_close==0 or die "c_close: $BerkeleyDB::Error, $!.";
- $cursor = undef;
- };
- $eval_stat = $@;
- if (defined $db) {
- $cursor->c_close if defined $cursor; # unlock, ignoring status
- $cursor = undef;
- # if ($eval_stat eq '') {
- # my($stat) = $db->db_sync(); # not really needed
- # $stat==0 or warn "BDB N db_sync, status $stat: $BerkeleyDB::Error, $!.";
- # }
- }
- if ($interrupt ne '') { kill($interrupt,$$) } # resignal
- elsif ($eval_stat ne '')
- { chomp($eval_stat); die "register_proc: BDB N $eval_stat\n" }
- }
- 1;
- #
- package Amavis::DB;
- use strict;
- use re 'taint';
- BEGIN {
- import Amavis::Conf qw($db_home $daemon_chroot_dir);
- import Amavis::Util qw(untaint ll do_log);
- }
- use BerkeleyDB;
- BEGIN {
- use Exporter ();
- use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.043';
- @ISA = qw(Exporter);
- }
- # create new databases, then close them (called by the parent process)
- # (called only if $db_home is nonempty)
- sub init($) {
- my($predelete) = @_; # delete existing db files first?
- my($name) = $db_home;
- $name = "$daemon_chroot_dir $name" if $daemon_chroot_dir ne '';
- if ($predelete) { # delete old database files
- local(*DIR);
- opendir(DIR,$db_home) or die "db_init: Can't open directory $name: $!";
- my(@dirfiles) = readdir(DIR); #must avoid modifying dir while traversing it
- closedir(DIR) or die "db_init: Error closing directory $name: $!";
- for my $f (@dirfiles) {
- next if ($f eq '.' || $f eq '..') && -d _;
- if ($f =~ /^(__db\.\d+|(cache-expiry|cache|snmp|nanny)\.db)\z/s) {
- $f = untaint($f);
- unlink("$db_home/$f") or die "db_init: Can't delete file $name/$f: $!";
- }
- }
- }
- undef $!; my($env) = BerkeleyDB::Env->new(-Home=>$db_home, -Mode=>0640,
- -Flags=> DB_CREATE | DB_INIT_CDB | DB_INIT_MPOOL);
- defined $env
- or die "db_init: BDB bad db env. at $db_home: $BerkeleyDB::Error, $!.";
- do_log(0, sprintf("Creating db in %s/; BerkeleyDB %s, libdb %s",
- $name, BerkeleyDB->VERSION, $BerkeleyDB::db_version));
- undef $!; my($dbc) = BerkeleyDB::Hash->new(
- -Filename=>'cache.db', -Flags=>DB_CREATE, -Env=>$env );
- defined $dbc or die "db_init: BDB no dbC: $BerkeleyDB::Error, $!.";
- undef $!; my($dbq) = BerkeleyDB::Queue->new(
- -Filename=>'cache-expiry.db', -Flags=>DB_CREATE, -Env=>$env,
- -Len=>15+1+32 ); # '-ExtentSize' needs DB 3.2.x, e.g. -ExtentSize=>2
- defined $dbq or die "db_init: BDB no dbQ: $BerkeleyDB::Error, $!.";
- undef $!; my($dbs) = BerkeleyDB::Hash->new(
- -Filename=>'snmp.db', -Flags=>DB_CREATE, -Env=>$env );
- defined $dbs or die "db_init: BDB no dbS: $BerkeleyDB::Error, $!.";
- undef $!; my($dbn) = BerkeleyDB::Hash->new(
- -Filename=>'nanny.db', -Flags=>DB_CREATE, -Env=>$env );
- defined $dbn or die "db_init: BDB no dbN: $BerkeleyDB::Error, $!.";
- Amavis::DB::SNMP::put_initial_snmp_data($dbs);
- for my $db ($dbc, $dbq, $dbs, $dbn) {
- $db->db_close==0 or die "db_init: BDB db_close: $BerkeleyDB::Error, $!.";
- }
- }
- # open an existing databases environment (called by each child process)
- sub new {
- my($class) = @_; my($env);
- if (defined $db_home) {
- $env = BerkeleyDB::Env->new(
- -Home=>$db_home, -Mode=>0640, -Flags=> DB_INIT_CDB | DB_INIT_MPOOL);
- defined $env or die "BDB bad db env. at $db_home: $BerkeleyDB::Error, $!.";
- }
- bless \$env, $class;
- }
- sub get_db_env { my($self) = shift; $$self }
- 1;
- __DATA__
- #
- package Amavis::Cache;
- # offer an 'IPC::Cache'-compatible interface to a BerkeleyDB-based cache.
- # Replaces methods new,get,set of the memory-based cache.
- use strict;
- use re 'taint';
- BEGIN {
- import Amavis::Util qw(ll do_log);
- }
- use BerkeleyDB;
- BEGIN {
- use Exporter ();
- use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.0432';
- @ISA = qw(Exporter);
- }
- # open existing databases (called by each child process);
- # if $db_env is undef a memory-based cache is created, otherwise use BerkeleyDB
- sub new {
- my($class,$db_env) = @_;
- my($dbc,$dbq,$mem_cache);
- if (!defined($db_env)) {
- do_log(1,"BerkeleyDB not available, using memory-based local cache");
- $mem_cache = {};
- } else {
- my($env) = $db_env->get_db_env;
- defined $env or die "BDB bad db env.: $BerkeleyDB::Error, $!.";
- $dbc = BerkeleyDB::Hash->new(-Filename=>'cache.db', -Env=>$env);
- defined $dbc or die "BDB no dbC: $BerkeleyDB::Error, $!.";
- $dbq = BerkeleyDB::Queue->new(-Filename=>'cache-expiry.db', -Env=>$env,
- -Len=>15+1+32); # '-ExtentSize' needs DB 3.2.x, e.g. -ExtentSize=>2
- defined $dbq or die "BDB no dbQ: $BerkeleyDB::Error, $!.";
- }
- bless {'db_cache'=>$dbc, 'db_queue'=>$dbq, 'mem_cache'=>$mem_cache}, $class;
- }
- sub DESTROY {
- my($self) = shift;
- eval { do_log(5,"Amavis::Cache DESTROY called") };
- for my $db ($self->{'db_cache'}, $self->{'db_queue'}) {
- if (defined $db) {
- eval { $db->db_close==0 or die "db_close: $BerkeleyDB::Error, $!." };
- if ($@ ne '') { warn "BDB C+Q DESTROY $@" }
- $db = undef;
- }
- }
- }
- # purge expired entries from the queue head and enqueue new entry at the tail
- sub enqueue {
- my($self,$str,$now_utc_iso8601,$expires_utc_iso8601) = @_;
- my($db) = $self->{'db_cache'}; my($dbq) = $self->{'db_queue'};
- local($1,$2); my($stat,$key,$val); $key = '';
- my($qcursor) = $dbq->db_cursor(DB_WRITECURSOR);
- defined $qcursor or die "BDB Q db_cursor: $BerkeleyDB::Error, $!.";
- # no warnings 'numeric'; # seems like c_get can return an empty string?!
- while ( $stat=$qcursor->c_get($key,$val,DB_NEXT), $stat eq '' || $stat==0 ) {
- do_log(5,"enqueue: stat is not numeric: \"$stat\"") if $stat !~ /^\d+\z/;
- if ($val !~ /^([^ ]+) (.*)\z/s) {
- do_log(-2,"WARN: queue head invalid, deleting: $val");
- } else {
- my($t,$digest) = ($1,$2);
- last if $t ge $now_utc_iso8601;
- my($cursor) = $db->db_cursor(DB_WRITECURSOR);
- defined $cursor or die "BDB C db_cursor: $BerkeleyDB::Error, $!.";
- my($v); my($st1) = $cursor->c_get($digest,$v,DB_SET);
- $st1==0 || $st1==DB_NOTFOUND or die "BDB C c_get: $BerkeleyDB::Error, $!.";
- if ($st1==0 && $v=~/^([^ ]+) /s) { # record exists and appears valid
- if ($1 ne $t) {
- do_log(5,"enqueue: not deleting: $digest, was refreshed since");
- } else { # its expiration time correspond to timestamp in the queue
- do_log(5,"enqueue: deleting: $digest");
- my($st2) = $cursor->c_del; # delete expired entry from the cache
- $st2==0 || $st2==DB_KEYEMPTY
- or die "BDB C c_del: $BerkeleyDB::Error, $!.";
- }
- }
- $cursor->c_close==0 or die "BDB C c_close: $BerkeleyDB::Error, $!.";
- }
- my($st3) = $qcursor->c_del;
- $st3==0 || $st3==DB_KEYEMPTY or die "BDB Q c_del: $BerkeleyDB::Error, $!.";
- }
- $stat==0 || $stat==DB_NOTFOUND or die "BDB Q c_get: $BerkeleyDB::Error, $!.";
- $qcursor->c_close==0 or die "BDB Q c_close: $BerkeleyDB::Error, $!.";
- # insert new expiration request in the queue
- $dbq->db_put($key, "$expires_utc_iso8601 $str", DB_APPEND) == 0
- or die "BDB Q db_put: $BerkeleyDB::Error, $!.";
- # syncing would only be worth doing if we would want the cache to persist
- # across restarts - but we scratch the databases to avoid rebuild worries
- # $stat = $dbq->db_sync();
- # $stat==0 or warn "BDB Q db_sync, status $stat: $BerkeleyDB::Error, $!.";
- # $stat = $db->db_sync();
- # $stat==0 or warn "BDB C db_sync, status $stat: $BerkeleyDB::Error, $!.";
- }
- sub get {
- my($self,$key) = @_;
- my($val); my($db) = $self->{'db_cache'};
- if (!defined($db)) {
- $val = $self->{'mem_cache'}{$key}; # simple local memory-based cache
- } else {
- my($stat) = $db->db_get($key,$val);
- $stat==0 || $stat==DB_NOTFOUND
- or die "BDB C c_get: $BerkeleyDB::Error, $!.";
- local($1,$2);
- if ($stat==0 && $val=~/^([^ ]+) (.*)/s) { $val = $2 } else { $val = undef }
- }
- thaw($val);
- }
- sub set {
- my($self,$key,$obj,$now_utc_iso8601,$expires_utc_iso8601) = @_;
- my($db) = $self->{'db_cache'};
- if (!defined($db)) {
- $self->{'mem_cache'}{$key} = freeze($obj);
- } else {
- my($cursor) = $db->db_cursor(DB_WRITECURSOR);
- defined $cursor or die "BDB C db_cursor: $BerkeleyDB::Error, $!.";
- my($val); my($stat) = $cursor->c_get($key,$val,DB_SET);
- $stat==0 || $stat==DB_NOTFOUND
- or die "BDB C c_get: $BerkeleyDB::Error, $!.";
- $cursor->c_put($key, $expires_utc_iso8601.' '.freeze($obj),
- $stat==0 ? DB_CURRENT : DB_KEYLAST ) == 0
- or die "BDB C c_put: $BerkeleyDB::Error, $!.";
- $cursor->c_close==0 or die "BDB C c_close: $BerkeleyDB::Error, $!.";
- # $stat = $db->db_sync(); # only worth doing if cache were persistent
- # $stat==0 or warn "BDB C db_sync, status $stat: $BerkeleyDB::Error, $!.";
- $self->enqueue($key,$now_utc_iso8601,$expires_utc_iso8601);
- }
- $obj;
- }
- 1;
- __DATA__
- #^L
- package Amavis::Out::SQL::Connection;
- use strict;
- use re 'taint';
- BEGIN {
- use Exporter ();
- use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.043';
- @ISA = qw(Exporter);
- }
- use DBI;
- BEGIN {
- import Amavis::Conf qw(c cr ca);
- import Amavis::Util qw(ll do_log);
- import Amavis::Timing qw(section_time);
- }
- # one object per connection (normally exactly one) to a database server;
- # connection need not exist at all times, stores info on how to connect;
- # when connected it holds database handle
- sub new {
- my($class, @dsns) = @_; # a list of DSNs to try connecting to sequentially
- bless { dbh=>undef, sth=>undef, incarnation=>1, dsn_list=>\@dsns }, $class;
- }
- sub dbh { # get/set database handle
- my($self)=shift; !@_ ? $self->{dbh} : ($self->{dbh}=shift);
- }
- sub sth { # get/set statement handle
- my($self)=shift; my($clause)=shift;
- !@_ ? $self->{sth}{$clause} : ($self->{sth}{$clause}=shift);
- }
- sub dbh_inactive { # get/set dbh "InactiveDestroy" attribute
- my($self)=shift; my($dbh) = $self->dbh;
- if (!$dbh) { undef }
- else { !@_ ? $dbh->{'InactiveDestroy'} : ($dbh->{'InactiveDestroy'}=shift) }
- }
- sub DESTROY {
- my($self) = shift;
- eval { do_log(5,"Amavis::Out::SQL::Connection DESTROY called") };
- eval { $self->disconnect_from_sql };
- }
- # returns current connection version; works like cache versioning/invalidation:
- # SQL statement handles need to rebuilt and caches cleared when SQL connection
- # is re-established and a new database handle provided
- #
- sub incarnation { my($self)=shift; $self->{incarnation} }
- # DBI method wrappers:
- sub begin_work {
- my($self)=shift; do_log(5,"sql begin transaction");
- # DBD::mysql man page: if you detect an error while changing
- # the AutoCommit mode, you should no longer use the database handle.
- # In other words, you should disconnect and reconnect again
- $self->dbh or $self->connect_to_sql;
- eval { $self->dbh->begin_work(@_) };
- if ($@ ne '') {
- chomp($@); do_log(-1,"sql begin transaction failed, ".
- "probably disconnected by server, reconnecting ($@)");
- $self->disconnect_from_sql; $self->connect_to_sql;
- $self->dbh->begin_work(@_);
- }
- $self->{in_transaction} = 1;
- };
- sub begin_work_nontransaction {
- my($self)=shift; do_log(5,"sql begin, nontransaction");
- $self->dbh or $self->connect_to_sql;
- };
- sub commit {
- my($self)=shift; do_log(5,"sql commit");
- $self->{in_transaction} = 0;
- $self->dbh or die "commit: dbh not available";
- $self->dbh->commit(@_);
- };
- sub rollback {
- my($self)=shift; do_log(5,"sql rollback");
- $self->{in_transaction} = 0;
- $self->dbh or die "rollback: dbh not available";
- eval { $self->dbh->rollback(@_) };
- if ($@ ne '') {
- chomp($@); do_log(-1,"sql rollback error, reconnecting ($@)");
- $self->disconnect_from_sql; $self->connect_to_sql;
- # $self->dbh->rollback(@_); # too late now, hopefully implied in disconnect
- }
- };
- sub last_insert_id {
- my($self)=shift;
- $self->dbh or die "last_insert_id: dbh not available";
- $self->dbh->last_insert_id(@_);
- };
- sub fetchrow_arrayref {
- my($self,$clause,@args) = @_;
- $self->dbh or die "fetchrow_arrayref: dbh not available";
- $self->sth($clause) or die "fetchrow_arrayref: sth not available";
- $self->sth($clause)->fetchrow_arrayref(@args);
- };
- sub finish {
- my($self,$clause,@args) = @_;
- $self->dbh or die "finish: dbh not available";
- $self->sth($clause) or die "finish: sth not available";
- $self->sth($clause)->finish(@args);
- };
- sub execute {
- my($self,$clause,@args) = @_;
- $self->dbh or die "execute: dbh not available";
- my($sth) = $self->sth($clause); # fetch cached st. handle or prepare new
- if ($sth) {
- do_log(5,"sql: executing clause: $clause");
- } else {
- do_log(4,"sql: preparing and executing: $clause");
- $sth = $self->dbh->prepare($clause); $self->sth($clause,$sth);
- }
- eval { $sth->execute(@args) };
- if ($@ ne '') {
- my($err) = $@; chomp($err); my($msg) = "sql execute: sts=$DBI::err, $err";
- if (!$sth || ($sth->err ne '2006' && $sth->err ne '2013')) {
- die $msg;
- } else { # MySQL specific: server has gone away; Lost connection to...
- if ($self->{in_transaction}) {
- $self->disconnect_from_sql;
- die "sql execute failed within transaction, $msg";
- } else { # try one more time
- do_log(0,"NOTICE: reconnecting in response to: $msg");
- $self->disconnect_from_sql;
- $self->connect_to_sql;
- $self->dbh or die "execute: reconnect failed";
- do_log(4,"sql: preparing and executing (again): $clause");
- $sth = $self->dbh->prepare($clause); $self->sth($clause,$sth);
- eval { $sth->execute(@args) };
- if ($@ ne '') {
- $err = $@; chomp($err); $msg = "sql execute: sts=$DBI::err, $err";
- $self->disconnect_from_sql;
- die "failed again, $msg";
- }
- }
- }
- }
- 1;
- }
- # Connect to a database. Take a list of database connection
- # parameters and try each until one succeeds.
- # -- based on code from Ben Ransford <amavis@uce.ransford.org> 2002-09-22
- sub connect_to_sql {
- my($self) = shift; # a list of DSNs to try connecting to sequentially
- my($dbh); my(@dsns) = @{$self->{dsn_list}};
- do_log(3,"Connecting to SQL database server");
- for my $tmpdsn (@dsns) {
- my($dsn, $username, $password) = @$tmpdsn;
- do_log(4,"connect_to_sql: trying '$dsn'");
- $dbh = DBI->connect($dsn, $username, $password,
- {PrintError => 0, RaiseError => 0, Taint => 1, AutoCommit => 1} );
- if ($dbh) { do_log(3,"connect_to_sql: '$dsn' succeeded"); last }
- do_log(-1,"connect_to_sql: unable to connect to DSN '$dsn': ".$DBI::errstr);
- }
- $self->dbh($dbh); delete($self->{sth});
- $self->{in_transaction} = 0; $self->{incarnation}++;
- $dbh or die "connect_to_sql: unable to connect to any dataset";
- $dbh->{'RaiseError'} = 1;
- # $dbh->{mysql_auto_reconnect} = 1; # questionable benefit
- # $dbh->func(30000,'busy_timeout'); # milliseconds (SQLite)
- section_time('sql-connect');
- $self;
- }
- sub disconnect_from_sql($) {
- my($self) = shift; $self->{in_transaction} = 0;
- if ($self->dbh) {
- do_log(4,"disconnecting from SQL");
- $self->dbh->disconnect; $self->dbh(undef);
- }
- }
- 1;
- __DATA__
- #^L
- package Amavis::Out::SQL::Log;
- use strict;
- use re 'taint';
- BEGIN {
- use Exporter ();
- use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.043';
- @ISA = qw(Exporter);
- }
- use DBI;
- use Encode; # Perl 5.8 UTF-8 support
- BEGIN {
- import Amavis::Conf qw(:platform $myhostname c cr ca);
- import Amavis::rfc2821_2822_Tools qw(qquote_rfc2821_local split_address
- iso8601_utc_timestamp);
- import Amavis::Util qw(ll do_log am_id untaint safe_decode add_entropy);
- import Amavis::Out::SQL::Connection ();
- }
- sub new {
- my($class,$conn_h) = @_; bless { conn_h=>$conn_h, incarnation=>0 }, $class;
- }
- sub DESTROY {
- my($self) = shift;
- eval { do_log(5,"Amavis::Out::SQL::Log DESTROY called") };
- }
- sub save_info_preliminary {
- my($self, $conn,$msginfo) = @_;
- my($addr) = $msginfo->sender; my($invdomain) = '';
- if ($addr ne '') {
- local($1);
- my($localpart,$domain) = split_address($addr); $domain = lc($domain);
- $localpart = lc($localpart) if !c('localpart_is_case_sensitive');
- $domain = $1 if $domain=~/^\@?(.*?)\.*\z/s; # chop leading @ and trailing .
- $addr = $localpart.'@'.$domain;
- $addr = substr($addr,0,255) if length($addr) > 255;
- $invdomain = join('.', reverse split(/\./,$domain,-1));
- $invdomain = substr($invdomain,0,255) if length($invdomain) > 255;
- }
- my($conn_h) = $self->{conn_h}; my($sql_cl_r) = cr('sql_clause');
- $conn_h->begin_work; # SQL transaction starts
- eval {
- # find an existing e-mail address record for sender, or insert a new one
- my($sel_adr) = $sql_cl_r->{'sel_adr'};
- my($ins_adr) = $sql_cl_r->{'ins_adr'};
- my($sid,$a_ref);
- $conn_h->execute($sel_adr,untaint($addr));
- if ( defined($a_ref=$conn_h->fetchrow_arrayref($sel_adr)) ) {
- $sid = $a_ref->[0]; $conn_h->finish($sel_adr);
- } else { # does not exist, insert a new record for the e-mail address
- $conn_h->execute($ins_adr,untaint($addr),untaint($invdomain));
- $sid = $conn_h->last_insert_id(undef, undef, 'maddr', 'id');
- $sid = $conn_h->sth($ins_adr)->{'mysql_insertid'} if !defined($sid);
- if (defined $sid) { add_entropy($sid) }
- else { $sid = 0; do_log(1,"sql: DBD does not support last_insert_id") }
- }
- do_log(4,"save_info_preliminary: $sid, $addr, ".($a_ref?'exists':'new'));
- # insert a placeholder message record with sender information
- $conn_h->execute($sql_cl_r->{'ins_msg'},
- $msginfo->mail_id, $msginfo->secret_id, am_id(),
- $msginfo->rx_time, iso8601_utc_timestamp($msginfo->rx_time),
- untaint($sid), c('policy_bank_path'), untaint($msginfo->client_addr),
- untaint($msginfo->msg_size), substr($myhostname,0,255));
- $conn_h->commit;
- };
- if ($@ ne '') {
- my($err) = $@; chomp($err);
- eval { $conn_h->rollback };
- do_log(1, "save_info_preliminary: rollback".($@ eq '' ? " done" : ": $@"));
- do_log(-1, "WARN save_info_preliminary: $err");
- return 0;
- }
- 1;
- }
- sub save_info_final {
- my($self, $conn,$msginfo,$spam_level,$dsn_sent,$content_type) = @_;
- my($lpcs) = c('localpart_is_case_sensitive');
- my($mail_id) = $msginfo->mail_id;
- my($conn_h) = $self->{conn_h}; my($sql_cl_r) = cr('sql_clause');
- my($sel_adr,$ins_adr,$ins_rcp) = @$sql_cl_r{'sel_adr','ins_adr','ins_rcp'};
- $conn_h->begin_work; # SQL transaction starts
- eval {
- for my $r (@{$msginfo->per_recip_data}) {
- my($addr) = $r->recip_addr; my($invdomain) = '';
- if ($addr ne '') {
- local($1); my($localpart,$domain) = split_address($addr);
- $domain = lc($domain); $localpart = lc($localpart) if !$lpcs;
- $domain = $1 if $domain=~/^\@?(.*?)\.*\z/s; # chop leading @ and tr.dot
- $addr = $localpart.'@'.$domain;
- $addr = substr($addr,0,255) if length($addr) > 255;
- $invdomain = join('.', reverse split(/\./,$domain,-1));
- $invdomain = substr($invdomain,0,255) if length($invdomain) > 255;
- }
- # find an existing e-mail address record for recipients, or insert one
- my($rid,$a_ref);
- $conn_h->execute($sel_adr,untaint($addr));
- if (defined($a_ref=$conn_h->fetchrow_arrayref($sel_adr))) {
- $rid = $a_ref->[0]; $conn_h->finish($sel_adr);
- } else { # does not exist, insert a new record with the e-mail address
- $conn_h->execute($ins_adr,untaint($addr),untaint($invdomain));
- $rid = $conn_h->last_insert_id(undef, undef, 'maddr', 'id');
- $rid = $conn_h->sth($ins_adr)->{'mysql_insertid'} if !defined($rid);
- if (defined $rid) { add_entropy($rid) }
- else { $rid = 0; do_log(1,"sql: DBD does not support last_insert_id") }
- }
- do_log(4,"save_info_final $mail_id, recip id: $rid, $addr, ".
- ($a_ref?'exists':'new'));
- my($dest,$resp) = ($r->recip_destiny, $r->recip_smtp_response);
- my($d) = $resp=~/^4/ ? 'TEMPFAIL'
- : ($dest==D_BOUNCE && $resp=~/^5/) ? 'BOUNCE'
- : ($dest!=D_BOUNCE && $resp=~/^5/) ? 'REJECT'
- : ($dest==D_PASS && ($resp=~/^2/ || !$r->recip_done)) ? 'PASS'
- : ($dest==D_DISCARD) ? 'DISCARD' : '?';
- # insert recipient record
- $conn_h->execute($ins_rcp,
- $mail_id, untaint($rid), substr($d,0,1), ' ',
- $r->recip_blacklisted_sender ? 'Y' : 'N',
- $r->recip_whitelisted_sender ? 'Y' : 'N',
- !defined($spam_level) ? undef :
- untaint($spam_level)+$r->recip_score_boost,
- untaint($resp) );
- };
- my($m_id) = ''; my($from) = ''; my($subj) = '';
- my($ent) = $msginfo->mime_entity;
- if (!defined $ent) {
- do_log(4,"save_info_final: no MIME entity, header info not available");
- } else { # if message header has been parsed by MIME-Tools
- $m_id = $ent->head->get('Message-ID',0);
- $from = $ent->head->get('From',0);
- $subj = $ent->head->get('Subject',0);
- for ($m_id,$from,$subj) {
- local($1); chomp;
- s/\n([ \t])/$1/sg; s/^[ \t]+//s; s/[ \t]+\z//s; # unfold, trim
- if ($unicode_aware) {
- my($octets); # string of bytes (not logical chars), UTF8 encoded
- eval { $octets = Encode::encode_utf8(safe_decode('MIME-Header',$_))};
- if ($@ eq '') { $_ = $octets }
- else { do_log(1,"save_info_final INFO: header field ".
- "not decodable, keeping raw bytes: $@") }
- }
- $_ = substr($_,0,255) if length($_) > 255;
- }
- }
- my($quar_type) = $msginfo->quar_type;
- for ($quar_type,$content_type) { $_ = ' ' if !defined || /^ *\z/ }
- do_log(4,"save_info_final $mail_id, $quar_type, $content_type, $dsn_sent,".
- " $spam_level, Message-ID: $m_id, From: '$from', Subject: '$subj'");
- # update message record with additional information
- $conn_h->execute($sql_cl_r->{'upd_msg'},
- $content_type, $quar_type, $dsn_sent, untaint($spam_level),
- untaint($m_id), untaint($from), untaint($subj), $mail_id);
- $conn_h->commit;
- };
- if ($@ ne '') {
- my($err) = $@; chomp($err);
- eval { $conn_h->rollback };
- do_log(1, "save_info_final: rollback".($@ eq '' ? " done" : ": $@"));
- do_log(-1, "WARN save_info_final: $err");
- return 0;
- }
- 1;
- }
- 1;
- __DATA__
- #
- package Amavis::IO::SQL;
- # a simple IO wrapper around SQL for inserting/retrieving mail text
- # to/from a database
- use strict;
- use re 'taint';
- BEGIN {
- use Exporter ();
- use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.043';
- @ISA = qw(Exporter);
- }
- use Errno qw(ENOENT EACCES EIO);
- use DBI;
- BEGIN {
- import Amavis::Util qw(ll do_log untaint);
- }
- sub new {
- my($class) = shift; my($self) = bless {}, $class;
- if (@_) { $self->open(@_) or return undef }
- $self;
- }
- sub open {
- my($self) = shift; @$self{qw(conn_h clause dbkey mode maxbuf)} = @_;
- $self->{buf} = '';
- $self->{chunk_ind} = $self->{pos} = $self->{bufpos} = $self->{eof} = 0;
- if ($self->{mode} ne 'w') {
- eval { $self->{conn_h}->execute($self->{clause}, $self->{dbkey}) };
- my($ll) = $@ ne '' ? -1 : 4;
- ll($ll) && do_log($ll,sprintf("Amavis::IO::SQL::open (%s); key=%s: %s",
- $self->{clause}, $self->{dbkey}, $@));
- if ($@ ne '') {
- chomp($@); die "Amavis::IO::SQL::open error: $@";
- $! = EIO; return undef; # not reached
- }
- }
- $self;
- }
- sub DESTROY {
- my($self) = shift;
- if (ref $self && $self->{conn_h}) {
- eval { $self->close or die "Error closing: $!" };
- if ($@ ne '') { warn "Amavis::IO::SQL::close error: $@" }
- delete $self->{conn_h};
- }
- }
- sub close {
- my($self) = shift; $@ = undef;
- eval {
- if ($self->{mode} eq 'w') {
- $self->flush or die "Can't flush: $!";
- } elsif ($self->{conn_h} && $self->{clause} && !$self->{eof}) {
- # reading, closing before eof was reached
- $self->{conn_h}->finish($self->{clause}) or die "Can't finish: $!";
- }
- };
- delete @$self{
- qw(conn_h clause dbkey mode maxbuf buf chunk_ind pos bufpos eof) };
- if ($@ ne '') {
- chomp($@); die "Error closing, $@";
- $! = EIO; return undef; # not reached
- }
- 1;
- }
- sub seek {
- my($self,$pos,$whence) = @_;
- $whence==0 && $pos==0 or die "Seek to $whence,$pos on sql i/o not supported";
- ll(5) && do_log(5, "Amavis::IO::SQL::seek mode=".$self->{mode});
- $self->{mode} ne 'w'
- or die "Seek to $whence,$pos on sql i/o only supported for read mode";
- if ($self->{chunk_ind} <= 1) # still in the first chunk, just reset bufpos
- { $self->{pos} = $self->{bufpos} = $self->{eof} = 0; 1 } # reset, success
- else { # beyond the first chunk, need to restart the query from the beginning
- my($con,$clause,$key,$mode,$maxb) =
- @$self{qw(conn_h clause dbkey mode maxbuf)};
- $self->close or die "seek: error closing, $!";
- $self->open($con,$clause,$key,$mode,$maxb)
- or die "seek: reopen failed, $!";
- }
- 1;
- }
- sub read { # SCALAR,LENGTH,OFFSET
- my($self) = shift;
- !defined($_[2]) || $_[2]==0
- or die "Reading from sql to an offset not supported";
- my($req_len) = $_[1]; my($conn_h) = $self->{conn_h}; my($a_ref);
- ll(5) && do_log(5, "Amavis::IO::SQL::read, ".
- $self->{chunk_ind}.", ".$self->{bufpos});
- eval {
- while (!$self->{eof} && length($self->{buf})-$self->{bufpos} < $req_len) {
- $a_ref = $conn_h->fetchrow_arrayref($self->{clause});
- if (!defined($a_ref)) { $self->{eof} = 1 }
- else { $self->{buf} .= $a_ref->[0]; $self->{chunk_ind}++ }
- }
- };
- if ($@ ne '') {
- # we can't stash an arbitrary error message string into $!,
- # which forces us to use 'die' to properly report an error
- chomp($@); die "read: sql select failed, $@";
- $! = EIO; return undef; # not reached
- };
- $_[0] = substr($self->{buf}, $self->{bufpos}, $req_len);
- my($nbytes) = length($_[0]);
- $self->{bufpos} += $nbytes; $self->{pos} += $nbytes;
- if ($self->{bufpos} > 0 && $self->{chunk_ind} > 1) {
- # discard used-up part of the buf unless at ch.1, which may still be useful
- do_log(5,"read: moving on by ".$self->{bufpos}." chars");
- $self->{buf} = substr($self->{buf},$self->{bufpos}); $self->{bufpos} = 0;
- }
- $nbytes; # eof: 0, error: undef
- }
- sub getline {
- my($self) = shift; my($conn_h) = $self->{conn_h};
- ll(5) && do_log(5, "Amavis::IO::SQL::getline, ".
- $self->{chunk_ind}.", ".$self->{bufpos});
- my($a_ref,$line); my($ind) = -1;
- eval {
- while (!$self->{eof} &&
- ($ind=index($self->{buf},"\n",$self->{bufpos})) < 0) {
- $a_ref = $conn_h->fetchrow_arrayref($self->{clause});
- if (!defined($a_ref)) { $self->{eof} = 1 }
- else { $self->{buf} .= $a_ref->[0]; $self->{chunk_ind}++ }
- }
- };
- if ($@ ne '') {
- chomp($@); die "getline: reading sql select results failed, $@";
- $! = EIO; return undef; # not reached
- };
- if ($ind < 0 && $self->{eof}) # imply a NL before eof if missing
- { $self->{buf} .= "\n"; $ind = index($self->{buf}, "\n", $self->{bufpos}) }
- $ind >= 0 or die "Programming error, NL not found";
- if (length($self->{buf}) > $self->{bufpos}) { # nonempty buffer?
- $line = substr($self->{buf}, $self->{bufpos}, $ind+1-$self->{bufpos});
- my($nbytes) = length($line);
- $self->{bufpos} += $nbytes; $self->{pos} += $nbytes;
- if ($self->{bufpos} > 0 && $self->{chunk_ind} > 1) {
- # discard used-up part of the buf unless at ch.1, which may still be useful
- ll(5) && do_log(5,"getline: moving on by ".$self->{bufpos}." chars");
- $self->{buf} = substr($self->{buf},$self->{bufpos}); $self->{bufpos} = 0;
- }
- }
- # eof: undef, $! zero; error: undef, $! nonzero
- $! = 0; $line eq '' ? undef : $line;
- }
- sub flush {
- my($self) = shift;
- $self->{mode} eq 'w' or die "Can't flush, opened for reading";
- my($msg); my($conn_h) = $self->{conn_h};
- while (length($self->{buf}) > 0) {
- my($ind) = $self->{chunk_ind} + 1;
- ll(4) && do_log(4, sprintf("sql flush: key: (%s, %d), size=%d",
- $self->{dbkey}, $ind,
- length($self->{buf}) < $self->{maxbuf} ? length($self->{buf})
- : $self->{maxbuf} ));
- eval {
- $conn_h->execute($self->{clause}, $self->{dbkey}, $ind,
- untaint(substr($self->{buf},0,$self->{maxbuf})));
- };
- if ($@ ne '') { $msg = $@; last }
- substr($self->{buf},0,$self->{maxbuf}) = ''; $self->{chunk_ind} = $ind;
- }
- if (defined($msg)) {
- chomp($msg); $msg = "flush: sql inserting text failed, $msg";
- die $msg; # we can't stash an arbitrary error message string into $!,
- # which forces us to use 'die' to properly report an error
- $! = EIO; return undef; # not reached
- }
- 1;
- }
- sub print {
- my($self) = shift;
- $self->{mode} eq 'w' or die "Can't print, not opened for writing";
- my($nbytes); my($conn_h) = $self->{conn_h}; my($len) = length($_[0]);
- if ($len <= 0) { $nbytes = "0 but true" }
- else {
- $self->{buf} .= $_[0]; $self->{pos} += $len; $nbytes = $len;
- while (length($self->{buf}) >= $self->{maxbuf}) {
- my($ind) = $self->{chunk_ind} + 1;
- ll(4) && do_log(4, sprintf("sql print: key: (%s, %d), size=%d",
- $self->{dbkey}, $ind, $self->{maxbuf}));
- eval {
- $conn_h->execute($self->{clause}, $self->{dbkey}, $ind,
- untaint(substr($self->{buf},0,$self->{maxbuf})));
- };
- if ($@ ne '') {
- # we can't stash an arbitrary error message string into $!,
- # which forces us to use 'die' to properly report an error
- chomp($@); die "print: sql inserting mail text failed, $@";
- $! = EIO; return undef; # not reached
- };
- substr($self->{buf},0,$self->{maxbuf}) = ''; $self->{chunk_ind} = $ind;
- }
- }
- $nbytes;
- }
- sub printf { shift->print(sprintf(shift,@_)) }
- 1;
- #^L
- package Amavis::Out::SQL::Quarantine;
- use strict;
- use re 'taint';
- BEGIN {
- use Exporter ();
- use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.043';
- @ISA = qw(Exporter);
- @EXPORT = qw(&mail_via_sql);
- }
- use subs @EXPORT;
- use DBI;
- use IO::Wrap;
- BEGIN {
- import Amavis::Conf qw(:platform c cr ca);
- import Amavis::rfc2821_2822_Tools qw(qquote_rfc2821_local);
- import Amavis::Util qw(ll do_log am_id snmp_count);
- import Amavis::Timing qw(section_time);
- import Amavis::Out::SQL::Connection ();
- }
- sub mail_via_sql {
- my($conn_h,$msginfo,$initial_submission,$dsn_per_recip_capable,$filter) = @_;
- snmp_count('OutMsgs'); local($1);
- my($mail_id) = $msginfo->mail_id;
- my(@per_recip_data) = grep { !$_->recip_done && (!$filter || &$filter($_)) }
- @{$msginfo->per_recip_data};
- my($logmsg) = sprintf("%s via SQL: %s", ($initial_submission?'SEND':'FWD'),
- qquote_rfc2821_local($msginfo->sender));
- if (!@per_recip_data) { do_log(5, "$logmsg, nothing to do"); return 1 }
- do_log(1, $logmsg . " -> " .
- qquote_rfc2821_local(map {$_->recip_final_addr} @per_recip_data) .
- ", mail_id $mail_id");
- my($msg) = $msginfo->mail_text; # a scalar reference, or a file handle
- if (defined($msg) && !$msg->isa('MIME::Entity')) {
- $msg = IO::Wrap::wraphandle($msg); # now we have an IO::Handle-like obj
- $msg->seek(0,0) or die "Can't rewind mail file: $!";
- }
- eval {
- my($sql_cl_r) = cr('sql_clause');
- $conn_h->begin_work; # SQL transaction starts
- eval {
- my($mp) = Amavis::IO::SQL->new;
- $mp->open($conn_h, $sql_cl_r->{'ins_quar'},$msginfo->mail_id,'w',16384)
- or die "Can't open Amavis::IO::SQL object: $!";
- my($hdr_edits) = $msginfo->header_edits;
- $hdr_edits = Amavis::Out::EditHeader->new if !$hdr_edits;
- my($received_cnt) = $hdr_edits->write_header($msg,$mp);
- if ($received_cnt > 100) { # loop detection required by rfc2821 6.2
- die "Too many hops: $received_cnt 'Received:' header lines";
- } elsif (!defined($msg)) { # empty mail body
- } elsif ($msg->isa('MIME::Entity')) {
- $msg->print_body($mp);
- } else {
- my($nbytes,$buff);
- while (($nbytes=$msg->read($buff,16384)) > 0)
- { $mp->print($buff) or die "Can't write to SQL sorage: $!" }
- defined $nbytes or die "Error reading: $!";
- }
- $mp->close or die "Error closing Amavis::IO::SQL object: $!";
- $conn_h->commit;
- };
- if ($@ ne '') {
- my($msg) = $@; chomp($msg);
- $msg = "writing mail text to SQL failed: $msg"; do_log(0,$msg);
- eval { $conn_h->rollback };
- do_log(1, "mail_via_sql: rollback".($@ eq '' ? " done" : ": $@"));
- die $msg;
- }
- };
- my($err) = $@; my($smtp_response);
- if ($err eq '') {
- $smtp_response = "250 2.6.0 Ok, Stored to sql db as mail_id $mail_id";
- snmp_count('OutMsgsDelivers');
- } else {
- chomp($err);
- if ($err =~ /too many hops/i) {
- $smtp_response = "550 5.4.6 Rejected: $err";
- snmp_count('OutMsgsRejects');
- } else {
- $smtp_response = "451 4.5.0 Storing to sql db as mail_id $mail_id failed: $err";
- snmp_count('OutAttemptFails');
- }
- }
- $smtp_response .= ", id=" . am_id();
- for my $r (@per_recip_data) {
- next if $r->recip_done;
- $r->recip_smtp_response($smtp_response); $r->recip_done(2);
- $r->recip_mbxname($mail_id) if $smtp_response =~ /^2/;
- }
- section_time('fwd-sql');
- 1;
- }
- __DATA__
- #
- package Amavis::Lookup::SQLfield;
- use strict;
- use re 'taint';
- BEGIN {
- use Exporter ();
- use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.043';
- @ISA = qw(Exporter);
- }
- BEGIN { import Amavis::Util qw(ll do_log) }
- sub new($$$;$$) {
- my($class, $sql_query,$fieldname, $fieldtype,$implied_args) = @_;
- # fieldtype: B=boolean, N=numeric, S=string,
- # N-: numeric, nonexistent field returns undef without complaint
- # S-: string, nonexistent field returns undef without complaint
- # B-: boolean, nonexistent field returns undef without complaint
- # B0: boolean, nonexistent field treated as false
- # B1: boolean, nonexistent field treated as true
- return undef if !defined($sql_query);
- my($self) = bless {}, $class;
- $self->{sql_query} = $sql_query;
- $self->{fieldname} = lc($fieldname);
- $self->{fieldtype} = uc($fieldtype);
- $self->{args} = ref($implied_args) eq 'ARRAY' ? [@$implied_args] # copy
- : [$implied_args] if defined $implied_args;
- $self;
- }
- sub lookup_sql_field($$$) {
- my($self,$addr,$get_all) = @_;
- my(@result,@matchingkey);
- if (!defined($self)) {
- do_log(5, "lookup_sql_field - undefined, \"$addr\" no match");
- } elsif (!defined($self->{sql_query})) {
- do_log(5, sprintf("lookup_sql_field(%s) - null query, \"%s\" no match",
- $self->{fieldname}, $addr));
- } else {
- my($field) = $self->{fieldname};
- my($res_ref,$mk_ref) = $self->{sql_query}->lookup_sql($addr,1,
- !exists($self->{args}) ? () : $self->{args});
- do_log(5, "lookup_sql_field($field), \"$addr\" no matching records")
- if !defined($res_ref) || !@$res_ref;
- for my $ind (0 .. (!defined($res_ref) ? -1 : $#$res_ref)) {
- my($match); my($h_ref) = $res_ref->[$ind]; my($mk) = $mk_ref->[$ind];
- if (!exists($h_ref->{$field})) {
- # record found, but no field with that name in the table
- # fieldtype: B0: boolean, nonexistent field treated as false,
- # B1: boolean, nonexistent field treated as true
- if ( $self->{fieldtype} =~ /^B0/) { # boolean, defaults to false
- $match = 0; # nonexistent field treated as 0
- do_log(5, "lookup_sql_field($field), no field, \"$addr\" result=$match");
- } elsif ($self->{fieldtype} =~ /^B1/) { # defaults to true
- $match = 1; # nonexistent field treated as 1
- do_log(5,"lookup_sql_field($field), no field, \"$addr\" result=$match");
- } elsif ($self->{fieldtype}=~/^.-/s) { # allowed to not exist
- do_log(5,"lookup_sql_field($field), no field, \"$addr\" result=undef");
- } else { # treated as 'no match', issue a warning
- do_log(1,"lookup_sql_field($field) ".
- "(WARN: no such field in the SQL table), ".
- "\"$addr\" result=undef");
- }
- } else { # field exists
- # fieldtype: B=boolean, N=numeric, S=string
- $match = $h_ref->{$field};
- if (!defined($match)) { # NULL field values represented as undef
- } elsif ($self->{fieldtype} =~ /^B/) { # boolean
- # convert values 'N', 'F', '0', ' ' and "\000" to 0
- # to allow value to be used directly as a Perl boolean
- $match = 0 if $match =~ /^([NnFf ]|0+|\000+)[ ]*\z/;
- } elsif ($self->{fieldtype} =~ /^N/) { # numeric
- $match = $match + 0; # unify different numeric forms
- } elsif ($self->{fieldtype} =~ /^S/) { # string
- $match =~ s/ +\z//; # trim trailing spaces
- }
- do_log(5, "lookup_sql_field($field) \"$addr\" result=" .
- (defined $match ? $match : 'undef') );
- }
- if (defined $match) {
- push(@result,$match); push(@matchingkey,$mk);
- last if !$get_all;
- }
- }
- }
- if (!$get_all) { !wantarray ? $result[0] : ($result[0], $matchingkey[0]) }
- else { !wantarray ? \@result : (\@result, \@matchingkey) }
- }
- 1;
- #
- package Amavis::Lookup::SQL;
- use strict;
- use re 'taint';
- BEGIN {
- use Exporter ();
- use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.043';
- @ISA = qw(Exporter);
- }
- use DBI;
- BEGIN {
- import Amavis::Conf qw(:platform :confvars c cr ca);
- import Amavis::Timing qw(section_time);
- import Amavis::Util qw(untaint snmp_count ll do_log);
- import Amavis::rfc2821_2822_Tools qw(make_query_keys);
- import Amavis::Out::SQL::Connection ();
- }
- # return a new Lookup::SQL object to contain DBI handle and prepared selects
- sub new {
- my($class, $conn_h, $clause_name) = @_;
- if ($clause_name eq '') { undef }
- else {
- # $clause_name is an key into %sql_clause of the currently selected
- # policy bank; one level of indirection is allowed in %sql_clause result,
- # the resulting SQL clause may include %k, to be expanded
- bless { conn_h => $conn_h, incarnation => 0, clause_name => $clause_name },
- $class;
- }
- }
- sub DESTROY {
- my($self) = shift; eval { do_log(5,"Amavis::Lookup::SQL DESTROY called") };
- }
- sub init {
- my($self) = @_;
- if ($self->{incarnation} != $self->{conn_h}->incarnation) { # invalidated?
- $self->{incarnation} = $self->{conn_h}->incarnation;
- $self->clear_cache; # db handle has changed, invalidate cache
- }
- $self;
- }
- sub clear_cache {
- my($self) = @_;
- delete $self->{cache};
- }
- # lookup_sql() performs a lookup for an e-mail address against a SQL map.
- # If a match is found it returns whatever the map returns (a reference
- # to a hash containing values of requested fields), otherwise returns undef.
- # A match aborts further fetching sequence, unless $get_all is true.
- #
- # SQL lookups (e.g. for user+foo@example.com) are performed in order
- # which can be requested by 'ORDER BY' in the SELECT statement, otherwise
- # the order is unspecified, which is only useful if only specific entries
- # exist in a database (e.g. only full addresses, not domains).
- #
- # The following order is recommended, going from specific to more general:
- # - lookup for user+foo@example.com
- # - lookup for user@example.com (only if $recipient_delimiter nonempty)
- # - lookup for user+foo ('naked lookup': only if local)
- # - lookup for user ('naked lookup': local and $recipient_delimiter nonempty)
- # - lookup for @sub.example.com
- # - lookup for @.sub.example.com
- # - lookup for @.example.com
- # - lookup for @.com
- # - lookup for @. (catchall)
- # NOTE:
- # this is different from hash and ACL lookups in two important aspects:
- # - a key without '@' implies mailbox (=user) name, not domain name;
- # - the naked mailbox name lookups are only performed when the e-mail addr
- # (usually its domain part) matches the static local_domains* lookups.
- #
- # The domain part is always lowercased when constructing a key,
- # the localpart is lowercased unless $localpart_is_case_sensitive is true.
- #
- sub lookup_sql($$$;$) {
- my($self, $addr,$get_all,$extra_args) = @_;
- my(@matchingkey,@result);
- my($sel); my($sql_cl_r) = cr('sql_clause');
- $sel = $sql_cl_r->{$self->{clause_name}} if defined $sql_cl_r;
- $sel = $$sel if ref $sel eq 'SCALAR'; # allow one level of indirection
- if (!defined($sel) || $sel eq '') {
- ll(4) && do_log(4,"lookup_sql disabled for clause: ".$self->{clause_name});
- return(!wantarray ? undef : (undef,undef));
- } elsif (!defined $extra_args &&
- exists $self->{cache} && exists $self->{cache}->{$addr})
- { # cached ?
- my($c) = $self->{cache}->{$addr}; @result = @$c if ref $c;
- @matchingkey = map {'/cached/'} @result; #will do for now, improve some day
- # if (!ll(5)) {}# don't bother preparing log report which will not be printed
- # elsif (!@result) { do_log(5,"lookup_sql (cached): \"$addr\" no match") }
- # else {
- # for my $m (@result) {
- # do_log(5, sprintf("lookup_sql (cached): \"%s\" matches, result=(%s)",
- # $addr, join(", ", map { sprintf("%s=>%s", $_,
- # !defined($m->{$_})?'-':'"'.$m->{$_}.'"'
- # ) } sort keys(%$m) ) ));
- # }
- # }
- if (!$get_all) {
- return(!wantarray ? $result[0] : ($result[0], $matchingkey[0]));
- } else {
- return(!wantarray ? \@result : (\@result, \@matchingkey));
- }
- }
- my($is_local); # $local_domains_sql is not looked up to avoid recursion!
- $is_local = Amavis::Lookup::lookup(0,$addr,
- grep {ref ne 'Amavis::Lookup::SQL' &&
- ref ne 'Amavis::Lookup::SQLfield' &&
- ref ne 'Amavis::Lookup::LDAP' &&
- ref ne 'Amavis::Lookup::LDAPattr'}
- @{ca('local_domains_maps')});
- my($keys_ref,$rhs_ref) = make_query_keys($addr,0,$is_local);
- my($n) = sprintf("%d",scalar(@$keys_ref)); # number of keys
- my(@pos_args); my(@extras_tmp) = !ref $extra_args ? () : @$extra_args;
- $sel =~ s{ ( %k | \? ) } # substitute %k for keys and ? for each extra arg
- { push(@pos_args, map { untaint($_) }
- $1 eq '%k' ? @$keys_ref : shift @extras_tmp),
- $1 eq '%k' ? join(',', ('?') x $n) : '?' }gxe;
- ll(4) && do_log(4,"lookup_sql \"$addr\", query args: ".
- join(', ', map{"\"$_\""} @pos_args));
- ll(4) && do_log(4,"lookup_sql select: $sel");
- my($a_ref,$found); my($match) = {}; my($conn_h) = $self->{conn_h};
- $conn_h->begin_work_nontransaction; # (re)connect if not connected
- eval {
- snmp_count('OpsSqlSelect');
- $conn_h->execute($sel,@pos_args); # do the query
- # fetch query results
- while ( defined($a_ref=$conn_h->fetchrow_arrayref($sel)) ) {
- my(@names) = @{$conn_h->sth($sel)->{NAME_lc}};
- $match = {}; @$match{@names} = @$a_ref;
- if (!exists $match->{'local'} && $match->{'email'} eq '@.') {
- # UGLY HACK to let a catchall (@.) imply that field 'local' has
- # a value undef (NULL) when that field is not present in the
- # database. This overrides B1 fieldtype default by an explicit
- # undef for '@.', causing a fallback to static lookup tables.
- # The purpose is to provide a useful default for local_domains
- # lookup if the field 'local' is not present in the SQL table.
- # NOTE: field names 'local' and 'email' are hardwired here!!!
- push(@names,'local'); $match->{'local'} = undef;
- do_log(5, "lookup_sql: \"$addr\" matches catchall, local=>undef");
- }
- push(@result, {%$match}); # copy hash
- push(@matchingkey, join(", ", map { sprintf("%s=>%s", $_,
- !defined($match->{$_})?'-':'"'.$match->{$_}.'"'
- ) } @names));
- last if !$get_all;
- }
- $conn_h->finish($sel) if defined $a_ref; # only if not all read
- }; # eval
- if ($@ ne '') {
- my($err) = $@; chomp($err);
- do_log(-1, "lookup_sql: $err, $DBI::err, $DBI::errstr");
- die $err;
- }
- if (!ll(4)) {
- # don't bother preparing log report which will not be printed
- } elsif (!@result) {
- do_log(4, "lookup_sql, \"$addr\" no match")
- } else {
- do_log(4, "lookup_sql($addr) matches, result=($_)") for @matchingkey;
- }
- # save for future use, but only within processing of this message
- $self->{cache}->{$addr} = \@result;
- section_time('lookup_sql');
- if (!$get_all) { !wantarray ? $result[0] : ($result[0], $matchingkey[0]) }
- else { !wantarray ? \@result : (\@result, \@matchingkey) }
- }
- 1;
- __DATA__
- #^L
- package Amavis::LDAP::Connection;
- use strict;
- use re 'taint';
- BEGIN {
- use Exporter ();
- use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION
- $ldap_sys_default);
- $VERSION= '2.043';
- @ISA = qw(Exporter);
- import Amavis::Conf qw(:platform :confvars c cr ca);
- import Amavis::Util qw(ll do_log);
- import Amavis::Timing qw(section_time);
- $ldap_sys_default = {
- hostname => 'localhost',
- port => 389,
- version => 3,
- timeout => 120,
- tls => 0,
- bind_dn => undef,
- bind_password => undef,
- };
- }
- sub new {
- my($class,$default) = @_;
- my($self) = bless {}, $class;
- $self->{ldap} = undef;
- $self->{incarnation} = 1;
- $ldap_sys_default->{port} = 636 if $default->{hostname} =~ /^ldaps/;
- for (qw(hostname port timeout tls base scope bind_dn bind_password)) {
- # replace undefined attributes with user values or defaults
- $self->{$_} = $default->{$_} unless defined($self->{$_});
- $self->{$_} = $ldap_sys_default->{$_} unless defined($self->{$_});
- }
- $self;
- }
- sub ldap { # get/set ldap handle
- my($self)=shift;
- !@_ ? $self->{ldap} : ($self->{ldap}=shift);
- }
- sub DESTROY {
- my($self)=shift;
- eval { do_log(5,"Amavis::LDAP::Connection DESTROY called") };
- eval { $self->disconnect_from_ldap };
- }
- sub incarnation { my($self)=shift; $self->{incarnation} }
- sub begin_work {
- my($self)=shift;
- do_log(5,"ldap begin_work");
- $self->ldap or $self->connect_to_ldap;
- }
- sub connect_to_ldap {
- my($self) = shift;
- my($bind_err,$start_tls_err);
- do_log(3,"Connecting to LDAP server");
- my $hostlist = ref $self->{hostname} eq 'ARRAY' ?
- join(", ",@{$self->{hostname}}) : $self->{hostname};
- do_log(4,"connect_to_ldap: trying $hostlist");
- my $ldap = Net::LDAP->new($self->{hostname},
- port => $self->{port},
- version => $self->{version},
- timeout => $self->{timeout},
- );
- if ($ldap) {
- do_log(3,"connect_to_ldap: connected to $hostlist");
- if ($self->{tls}) { # TLS required
- my($mesg) = $ldap->start_tls(verify=>'none');
- if ($mesg->code) { # start TLS failed
- my($err) = $mesg->error_name;
- do_log(-1,"connect_to_ldap: start TLS failed: $err");
- $self->ldap(undef);
- $start_tls_err = 1;
- } else { # started TLS
- do_log(3,"connect_to_ldap: TLS version $mesg enabled");
- }
- }
- if ($self->{bind_dn}) { # bind required
- my($mesg) = $ldap->bind($self->{bind_dn},
- password => $self->{bind_password});
- if ($mesg->code) { # bind failed
- my($err) = $mesg->error_name;
- do_log(-1,"connect_to_ldap: bind failed: $err");
- $self->ldap(undef);
- $bind_err = 1;
- } else { # bind succeeded
- do_log(3,"connect_to_ldap: bind $self->{bind_dn} succeeded");
- }
- }
- } else { # connect failed
- do_log(-1,"connect_to_ldap: unable to connect to host $hostlist");
- }
- $self->ldap($ldap); $self->{incarnation}++;
- $ldap or die "connect_to_ldap: unable to connect";
- if ($start_tls_err) { die "connect_to_ldap: start TLS failed" }
- if ($bind_err) { die "connect_to_ldap: bind failed" }
- section_time('ldap-connect');
- $self;
- }
- sub disconnect_from_ldap {
- my($self)=shift;
- if ($self->ldap) {
- do_log(4,"disconnecting from LDAP");
- $self->ldap->disconnect;
- $self->ldap(undef);
- }
- }
- sub do_search {
- my($self,$base,$scope,$filter) = @_;
- my($result);
- $self->ldap or die "do_search: ldap not available";
- do_log(5,sprintf(
- "lookup_ldap: searching base=\"%s\", scope=\"%s\", filter=\"%s\"",
- $base, $scope, $filter));
- eval {
- $result = $self->{ldap}->search(base => $base,
- scope => $scope,
- filter => $filter,
- );
- if ($result->code) { die $result->error_name, "\n"; }
- };
- if ($@ ne '') {
- my($err) = $@; chomp $err;
- if ($err =~ /^LDAP_/) { # LDAP related error
- do_log(0, "NOTICE: do_search: trying again: $err");
- $self->disconnect_from_ldap;
- $self->connect_to_ldap;
- $self->ldap or die "do_search: reconnect failed";
- do_log(5,sprintf(
- "lookup_ldap: searching (again) base=\"%s\", scope=\"%s\", filter=\"%s\"", $base, $scope, $filter));
- eval {
- $result = $self->{ldap}->search(base => $base,
- scope => $scope,
- filter => $filter,
- );
- if ($result->code) { die $result->error_name, "\n"; }
- };
- if (@_ ne '') {
- my($err) = $@; chomp $err;
- $self->disconnect_from_ldap;
- die "do_search: failed again, $err";
- }
- }
- die "do_search: $err";
- }
- return $result;
- }
- 1;
- #
- package Amavis::Lookup::LDAPattr;
- use strict;
- use re 'taint';
- BEGIN {
- use Exporter ();
- use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.043';
- @ISA = qw(Exporter);
- import Amavis::Util qw(ll do_log)
- }
- # attrtype: B=boolean, N=numeric, S=string, L=list
- # N-: numeric, nonexistent field returns undef without complaint
- # S-: string, nonexistent field returns undef without complaint
- # L-: list, nonexistent field returns undef without complaint
- # B-: boolean, nonexistent field returns undef without complaint
- # B0: boolean, nonexistent field treated as false
- # B1: boolean, nonexistent field treated as true
- sub new($$$;$) {
- my($class,$ldap_query,$attrname,$attrtype) = @_;
- return undef if !defined($ldap_query);
- my($self) = bless {}, $class;
- $self->{ldap_query} = $ldap_query;
- $self->{attrname} = lc($attrname);
- $self->{attrtype} = uc($attrtype);
- $self;
- }
- sub lookup_ldap_attr($$$) {
- my($self,$addr,$get_all) = @_;
- my(@result,@matchingkey);
- if (!defined($self)) {
- do_log(5,"lookup_ldap_attr - undefined, \"$addr\" no match");
- } elsif (!defined($self->{ldap_query})) {
- do_log(5,sprintf("lookup_ldap_attr(%s) - null query, \"%s\" no match",
- $self->{attrname}, $addr));
- } else {
- my($attr) = $self->{attrname};
- my($res_ref,$mk_ref) = $self->{ldap_query}->lookup_ldap($addr,1);
- do_log(5,"lookup_ldap_attr($attr), \"$addr\" no matching records")
- if !defined($res_ref) || !@$res_ref;
- for my $ind (0 .. (!defined($res_ref) ? -1 : $#$res_ref)) {
- my($match); my($h_ref) = $res_ref->[$ind]; my($mk) = $mk_ref->[$ind];
- if (!exists($h_ref->{$attr})) {
- # record found, but no attribute with that name in the table
- if ( $self->{attrtype} =~ /^B0/) { # boolean, defaults to false
- $match = 0; # nonexistent attribute treated as 0
- do_log(5,"lookup_ldap_attr($attr), no attribute, \"$addr\" result=$match");
- } elsif ($self->{attrtype} =~ /^B1/) { # boolean, defaults to true
- $match = 1; # nonexistent attribute treated as 1
- do_log(5,"lookup_ldap_attr($attr), no attribute, \"$addr\" result=$match");
- } elsif ($self->{attrtype}=~/^.-/s) { # allowed to not exist
- do_log(5,"lookup_ldap_attr($attr), no attribute, \"$addr\" result=undef");
- } else { # treated as 'no match', issue a warning
- do_log(1,"lookup_ldap_attr($attr) ".
- "(WARN: no such attribute in LDAP entry), ".
- "\"$addr\" result=undef");
- }
- } else { # attribute exists
- $match = $h_ref->{$attr};
- if (!defined($match)) { # NULL attribute values represented as undef
- } elsif ($self->{attrtype} =~ /^B/) { # boolean
- $match = $match eq "TRUE" ? 1 : 0; # convert TRUE|FALSE to 1|0
- } elsif ($self->{attrtype} =~ /^N/) { # numeric
- $match = $match + 0; # unify different numeric forms
- } elsif ($self->{attrtype} =~ /^S/) { # string
- $match =~ s/ +\z//; # trim trailing spaces
- } elsif ($self->{attrtype} =~ /^L/) { # list
- #$match = join(", ",@$match);
- }
- do_log(5,sprintf("lookup_ldap_attr(%s) \"%s\" result=(%s)",
- $attr, $addr, defined($match) ? $match : 'undef'));
- }
- if (defined $match) {
- push(@result,$match); push(@matchingkey,$mk);
- last if !$get_all;
- }
- }
- }
- if (!$get_all) { !wantarray ? $result[0] : ($result[0], $matchingkey[0]) }
- else { !wantarray ? \@result : (\@result, \@matchingkey) }
- }
- 1;
- #
- package Amavis::Lookup::LDAP;
- use strict;
- use re 'taint';
- BEGIN {
- use Exporter ();
- use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION
- $ldap_sys_default @ldap_attrs @mv_ldap_attrs);
- $VERSION = '2.043';
- @ISA = qw(Exporter);
- import Amavis::Conf qw(:platform :confvars c cr ca);
- import Amavis::Timing qw(section_time);
- import Amavis::Util qw(untaint snmp_count ll do_log);
- import Amavis::rfc2821_2822_Tools qw(make_query_keys split_address);
- import Amavis::LDAP::Connection ();
- $ldap_sys_default = {
- base => undef,
- scope => 'sub',
- query_filter => '(&(objectClass=amavisAccount)(mail=%m))',
- };
- @ldap_attrs = qw(amavisVirusLover amavisSpamLover amavisBannedFilesLover
- amavisBadHeaderLover amavisBypassVirusChecks amavisBypassSpamChecks
- amavisBypassBannedChecks amavisBypassHeaderChecks amavisSpamTagLevel
- amavisSpamTag2Level amavisSpamKillLevel amavisSpamModifiesSubj
- amavisVirusQuarantineTo amavisSpamQuarantineTo amavisBannedQuarantineTo
- amavisBadHeaderQuarantineTo amavisBlacklistSender amavisWhitelistSender
- amavisLocal amavisMessageSizeLimit amavisWarnVirusRecip
- amavisWarnBannedRecip amavisWarnBadHeaderRecip amavisVirusAdmin
- amavisNewVirusAdmin amavisSpamAdmin amavisBannedAdmin
- amavisBadHeaderAdmin amavisBannedRuleNames
- );
- @mv_ldap_attrs = qw(amavisBlacklistSender amavisWhitelistSender
- amavisBannedRuleNames
- );
- }
- sub new {
- my($class,$default,$conn_h) = @_;
- my($self) = bless {}, $class;
- $self->{conn_h} = $conn_h;
- $self->{incarnation} = 0;
- for (qw(base scope query_filter)) {
- # replace undefined attributes with config values or defaults
- $self->{$_} = $default->{$_} unless defined($self->{$_});
- $self->{$_} = $ldap_sys_default->{$_} unless defined($self->{$_});
- }
- $self;
- }
- sub DESTROY {
- my($self) = shift;
- eval { do_log(5,"Amavis::Lookup::LDAP DESTROY called") };
- }
- sub init {
- my($self) = @_;
- if ($self->{incarnation} != $self->{conn_h}->incarnation) { # invalidated?
- $self->{incarnation} = $self->{conn_h}->incarnation;
- $self->clear_cache; # db handle has changed, invalidate cache
- }
- $self;
- }
- sub clear_cache {
- my($self) = @_;
- delete $self->{cache};
- }
- sub lookup_ldap($$$) {
- my($self,$addr,$get_all) = @_;
- my(@result,@matchingkey,@tmp_result,@tmp_matchingkey);
- if (exists $self->{cache} && exists $self->{cache}->{$addr}) { # cached?
- my($c) = $self->{cache}->{$addr}; @result = @$c if ref $c;
- @matchingkey = map {'/cached/'} @result; # will do for now, improve some day
- # if (!ll(5)) {
- # # don't bother preparing log report which will not be printed
- # } elsif (!@result) {
- # do_log(5,"lookup_ldap (cached): \"$addr\" no match");
- # } else {
- # for my $m (@result) {
- # do_log(5, sprintf("lookup_ldap (cached): \"%s\" matches, result=(%s)",
- # $addr, join(", ", map { sprintf("%s=>%s", $_,
- # !defined($m->{$_})?'-':'"'.$m->{$_}.'"'
- # ) } sort keys(%$m) ) ));
- # }
- # }
- if (!$get_all) {
- return(!wantarray ? $result[0] : ($result[0], $matchingkey[0]));
- } else {
- return(!wantarray ? \@result : (\@result, \@matchingkey));
- }
- }
- my($is_local); # LDAP is not looked up to avoid recursion!
- $is_local = Amavis::Lookup::lookup(0,$addr,
- grep {ref ne 'Amavis::Lookup::SQL' &&
- ref ne 'Amavis::Lookup::SQLfield' &&
- ref ne 'Amavis::Lookup::LDAP' &&
- ref ne 'Amavis::Lookup::LDAPattr'}
- @{ca('local_domains_maps')});
- my($keys_ref,$rhs_ref,@keys);
- ($keys_ref,$rhs_ref) = make_query_keys($addr,0,$is_local);
- @keys = @$keys_ref;
- unshift(@keys, '<>') if $addr eq ''; # a hack for a null return path
- $_ = untaint($_) for @keys; # untaint keys
- $_ = Net::LDAP::Util::escape_filter_value($_) for @keys;
- # process %m
- my @filter_attr;
- my $filter = $self->{query_filter};
- while ($filter =~ /%m/) {
- (my $filter_pair) = $filter =~ /\(([^(]*=%m)\)/;
- my ($filter_attr) = split(/=/, $filter_pair);
- my $filter_string = '|' . join('', map { "($filter_attr=$_)" } @keys);
- $filter =~ s/\Q$filter_pair\E/$filter_string/;
- push(@filter_attr, $filter_attr);
- }
- # process %d
- my($base) = $self->{base};
- if ($base =~ /%d/) {
- my($localpart,$domain) = split_address($addr);
- if ($domain) {
- $domain = untaint($domain); $domain = lc($domain);
- $domain =~ s/^\@?(.*?)\.*\z/$1/s;
- $base =~ s/%d/&Net::LDAP::Util::escape_dn_value($domain)/ge;
- }
- }
- # build hash of keys and array position
- my(%xref,$key_num);
- $xref{$_} = $key_num++ for @keys;
- #
- do_log(4,sprintf("lookup_ldap \"%s\", query keys: %s, base: %s, filter: %s",
- $addr,join(', ',map{"\"$_\""}@keys),$self->{base},$self->{query_filter}));
- my($conn_h) = $self->{conn_h};
- $conn_h->begin_work; # (re)connect if not connected
- eval {
- snmp_count('OpsLDAPSearch');
- my($result) = $conn_h->do_search($base, $self->{scope}, $filter );
- my(@entry) = $result->entries;
- for my $entry (@entry) {
- my($match) = {};
- $match->{dn} = $entry->dn;
- for my $attr (@ldap_attrs) {
- my($value);
- $attr = lc($attr);
- do_log(9,"lookup_ldap: reading attribute \"$attr\" from object");
- if (grep /^$attr\z/i, @mv_ldap_attrs) { # multivalued
- $value = $entry->get_value($attr, asref => 1);
- } else {
- $value = $entry->get_value($attr);
- }
- $match->{$attr} = $value if $value;
- }
- my $pos;
- for my $attr (@filter_attr) {
- my $value = $entry->get_value($attr);
- if ($value) {
- if (!exists $match->{'amavislocal'} && $value eq '@.') {
- # NOTE: see lookup_sql
- $match->{'amavislocal'} = undef;
- do_log(5,
- "lookup_ldap: \"$addr\" matches catchall, amavislocal=>undef");
- }
- $pos = $xref{$value};
- last;
- }
- }
- my $key_str = join(", ",map {sprintf("%s=>%s",$_,!defined($match->{$_})?
- '-':'"'.$match->{$_}.'"')} keys(%$match));
- push(@tmp_result, [$pos,{%$match}]); # copy hash
- push(@tmp_matchingkey, [$pos,$key_str]);
- last if !$get_all;
- }
- }; # eval
- if ($@ ne '') {
- my($err) = $@; chomp $err;
- do_log(-1,"lookup_ldap: $err");
- die $err;
- }
- @result = map {$_->[1]} sort {$a->[0] <=> $b->[0]} @tmp_result;
- @matchingkey = map {$_->[1]} sort {$a->[0] <=> $b->[0]} @tmp_matchingkey;
- if (!ll(4)) {
- # don't bother preparing log report which will not be printed
- } elsif (!@result) {
- do_log(4,"lookup_ldap, \"$addr\" no match")
- } else {
- do_log(4,"lookup_ldap($addr) matches, result=($_)") for @matchingkey;
- }
- # save for future use, but only within processing of this message
- $self->{cache}->{$addr} = \@result;
- section_time('lookup_ldap');
- if (!$get_all) { !wantarray ? $result[0] : ($result[0], $matchingkey[0]) }
- else { !wantarray ? \@result : (\@result, \@matchingkey) }
- }
- 1;
- __DATA__
- #
- package Amavis::In::AMCL;
- use strict;
- use re 'taint';
- BEGIN {
- use Exporter ();
- use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.043';
- @ISA = qw(Exporter);
- }
- use subs @EXPORT;
- use Errno qw(ENOENT EACCES);
- use IO::File ();
- use Digest::MD5;
- BEGIN {
- import Amavis::Conf qw(:platform :confvars c cr ca);
- import Amavis::Util qw(ll do_log debug_oneshot snmp_counters_init snmp_count
- am_id new_am_id untaint rmdir_recursively add_entropy);
- import Amavis::Lookup qw(lookup);
- import Amavis::Lookup::IP qw(lookup_ip_acl);
- import Amavis::Timing qw(section_time);
- import Amavis::rfc2821_2822_Tools;
- import Amavis::In::Message;
- import Amavis::In::Connection;
- import Amavis::IO::Zlib;
- import Amavis::Out::EditHeader qw(hdr);
- import Amavis::Out qw(mail_dispatch);
- import Amavis::Notify qw(msg_from_quarantine);
- }
- sub new($) { my($class) = @_; bless {}, $class }
- # used with sendmail milter and traditional (non-SMTP) MTA interface,
- # but also to request a message release from a quarantine
- #
- sub process_policy_request($$$$) {
- my($self, $sock, $conn, $check_mail, $old_amcl) = @_;
- # $sock: connected socket from Net::Server
- # $conn: information about client connection
- # $check_mail: subroutine ref to be called with file handle
- my(%attr);
- $0 = sprintf("amavisd (ch%d-P-idle)", $Amavis::child_invocation_count);
- do_log(5, "process_policy_request: $old_amcl, $0");
- if ($old_amcl) {
- # Accept a single request from traditional amavis helper program.
- # Receive TEMPDIR/SENDER/RCPTS/LDA/LDAARGS from client
- # Simple protocol: \2 means LDA follows; \3 means EOT (end of transmission)
- my($state) = 0; $attr{'request'} = 'AM.CL'; my($response) = "\001";
- my($rv,@recips,@ldaargs,$inbuff); local($1);
- my(@attr_names) = qw(tempdir sender recipient ldaargs);
- while (defined($rv = recv($sock, $inbuff, 8192, 0))) {
- $0 = sprintf("amavisd (ch%d-P)", $Amavis::child_invocation_count);
- if ($state < 2) {
- $attr{$attr_names[$state]} = $inbuff; $state++;
- } elsif ($state == 2 && $inbuff eq "\002") {
- $state++;
- } elsif ($state >= 2 && $inbuff eq "\003") {
- section_time('got data');
- $attr{'recipient'} = \@recips; $attr{'ldaargs'} = \@ldaargs;
- $attr{'delivery_care_of'} = @ldaargs ? 'client' : 'server';
- eval {
- my($msginfo) = preprocess_policy_query(\%attr);
- $response = (map { /^exit_code=(\d+)\z/ ? $1 : () }
- check_amcl_policy($conn,$msginfo,$check_mail,1))[0];
- };
- if ($@ ne '') {
- chomp($@); do_log(-2, "policy_server FAILED: $@");
- $response = EX_TEMPFAIL;
- }
- $state = 4;
- } elsif ($state == 2) {
- push(@recips, $inbuff);
- } else {
- push(@ldaargs, $inbuff);
- }
- defined send($sock,$response,0) or die "send failed in state $state: $!";
- last if $state >= 4;
- $0 = sprintf("amavisd (ch%d-P-idle)", $Amavis::child_invocation_count);
- }
- if ($state==4 && defined($rv)) {
- # normal termination
- } elsif (!defined($rv) && $! != 0) {
- die "recv failed in state $state: $!";
- } else { # eof or a runaway state
- die "helper client session terminated unexpectedly, state: $state";
- }
- do_log(2, Amavis::Timing::report()); # report elapsed times
- } else { # new amavis helper protocol AM.PDP or a Postfix policy server
- # for Postfix policy server see Postfix docs SMTPD_POLICY_README
- my(@response); local($1,$2,$3);
- local($/) = "\012"; # set line terminator to LF (Postfix idiosyncrasy)
- my($ln); # can accept multiple tasks
- for (undef $!; defined($ln=$sock->getline); undef $!) {
- $0 = sprintf("amavisd (ch%d-P)", $Amavis::child_invocation_count);
- Amavis::Timing::init(); snmp_counters_init();
- # must not use \r and \n, not \015 and \012 on certain platforms
- if ($ln =~ /^\015?\012\z/) { # end of request
- section_time('got data');
- eval {
- my($msginfo) = preprocess_policy_query(\%attr);
- @response = $attr{'request'} eq 'smtpd_access_policy'
- ? postfix_policy($conn,$msginfo,\%attr)
- : $attr{'request'} eq 'release'
- ? dispatch_from_quarantine($conn,$msginfo)
- : check_amcl_policy($conn,$msginfo,$check_mail,0);
- };
- if ($@ ne '') {
- chomp($@); do_log(-2, "policy_server FAILED: $@");
- @response = (proto_encode('setreply','450','4.5.0',"Failure: $@"),
- proto_encode('return_value','tempfail'),
- proto_encode('exit_code',sprintf("%d",EX_TEMPFAIL)));
- # last;
- }
- $sock->print( map { $_."\015\012" } (@response,'') )
- or die "Can't write response to socket: $!";
- %attr = (); @response = ();
- do_log(2, Amavis::Timing::report());
- } elsif ($ln =~ /^ ([^=\000\012]*?) (=|:[ \t]*)
- ([^\012]*?) \015?\012 \z/xsi) {
- my($attr_name) = Amavis::tcp_lookup_decode($1);
- my($attr_val) = Amavis::tcp_lookup_decode($3);
- if (!exists $attr{$attr_name}) {
- $attr{$attr_name} = $attr_val;
- } else {
- $attr{$attr_name} = [ $attr{$attr_name} ] if !ref $attr{$attr_name};
- push(@{$attr{$attr_name}}, $attr_val);
- }
- my($known_attr) = scalar(grep {$_ eq $attr_name} qw(
- request helo_name protocol_state protocol_name queue_id
- client_name client_address sender recipient
- mail_id secret_id quar_type mail_file) );
- do_log(!$known_attr?-1:1, "policy protocol: $attr_name=$attr_val");
- } else {
- do_log(-1, "policy protocol: INVALID ATTRIBUTE LINE: $ln");
- }
- $0 = sprintf("amavisd (ch%d-P-idle)", $Amavis::child_invocation_count);
- }
- defined $ln || $!==0 or die "Read from client socket FAILED: $!";
- };
- $0 = sprintf("amavisd (ch%d-P)", $Amavis::child_invocation_count);
- }
- # Based on given policy query attributes describing message to be cached
- # or released, return a new Amavis::In::Message object
- #
- sub preprocess_policy_query($) {
- my($attr_ref) = @_;
- my($msginfo) = Amavis::In::Message->new;
- $msginfo->rx_time(time); # now
- add_entropy(%$attr_ref);
- # amavisd -> amavis-helper protocol query consists of any number of
- # the following lines, the response is terminated by an empty line.
- # The 'request=AM.PDP' is a required first field, the order of
- # remaining fields is arbitrary, but multivalued attributes such as
- # 'recipient' must retain their relative order.
- # Required AM.PDP fields are: request, tempdir, sender, recipient(s)
- # request=AM.PDP
- # tempdir=/var/amavis/amavis-milter-MWZmu9Di
- # tempdir_removed_by=client (tempdir_removed_by=server is a default)
- # mail_file=/var/amavis/am.../email.txt (defaults to tempdir/email.txt)
- # sender=<foo@example.com>
- # recipient=<bar1@example.net>
- # recipient=<bar2@example.net>
- # recipient=<bar3@example.net>
- # delivery_care_of=server (client or server, client is a default)
- # queue_id=qid
- # protocol_name=ESMTP
- # helo_name=b.example.com
- # client_address=10.2.3.4
- # Required 'release' fields are: request, mail_id
- # request=release
- # mail_id=xxxxxxxxxxxx
- # secret_id=xxxxxxxxxxxx (authorizes a release)
- # quar_type=x F/Z/B/Q/M (defaults to Q or F)
- # file/zipfile/bsmtp/sql/mailbox
- # mail_file=... (optional: overrides automatics; $QUARANTINEDIR prepended)
- # requested_by=<releaser@example.com> (optional: lands in Resent-From:)
- # sender=<foo@example.com> (optional: replaces envelope sender)
- # recipient=<bar1@example.net> (optional: replaces envelope recips)
- # recipient=<bar2@example.net>
- # recipient=<bar3@example.net>
- my($sender,@recips);
- exists $attr_ref->{'request'} or die "Missing 'request' field";
- my($ampdp) = $attr_ref->{'request'} =~ /^AM\.CL|AM\.PDP|release\z/i;
- $msginfo->delivery_method(
- lc($attr_ref->{'delivery_care_of'}) eq 'server' ? c('forward_method') :'');
- $msginfo->client_delete(lc($attr_ref->{'tempdir_removed_by'}) eq 'client'
- ? 1 : 0);
- $msginfo->queue_id($attr_ref->{'queue_id'})
- if exists $attr_ref->{'queue_id'};
- $msginfo->client_addr($attr_ref->{'client_address'})
- if exists $attr_ref->{'client_address'};
- $msginfo->client_name($attr_ref->{'client_name'})
- if exists $attr_ref->{'client_name'};
- $msginfo->client_proto($attr_ref->{'protocol_name'})
- if exists $attr_ref->{'protocol_name'};
- $msginfo->client_helo($attr_ref->{'helo_name'})
- if exists $attr_ref->{'helo_name'};
- # $msginfo->body_type('8BITMIME'); # get_body_digest will set this if undef
- $msginfo->requested_by(unquote_rfc2821_local($attr_ref->{'requested_by'}))
- if exists $attr_ref->{'requested_by'};
- if (exists $attr_ref->{'sender'}) {
- $sender = $attr_ref->{'sender'};
- $sender = unquote_rfc2821_local($sender);
- $msginfo->sender($sender);
- }
- if (exists $attr_ref->{'recipient'}) {
- my($r) = $attr_ref->{'recipient'};
- @recips = !ref($r) ? $r : @$r;
- $_ = unquote_rfc2821_local($_) for @recips;
- $msginfo->recips(\@recips);
- }
- if (!exists $attr_ref->{'tempdir'}) {
- $msginfo->mail_tempdir($TEMPBASE); # defaults to $TEMPBASE
- } else {
- local($1,$2); my($tempdir) = $attr_ref->{tempdir};
- $tempdir =~ /^ (?: \Q$TEMPBASE\E | \Q$MYHOME\E )
- \/ (?! \.{1,2} \z) [A-Za-z0-9_.-]+ \z/xso
- or die "Invalid/unexpected temporary directory name '$tempdir'";
- $msginfo->mail_tempdir(untaint($tempdir));
- }
- my($quar_type);
- if (!$ampdp) {} # don't bother with filenames
- elsif ($attr_ref->{'request'} eq 'release') {
- exists $attr_ref->{'mail_id'} or die "Missing 'mail_id' field";
- my($fn) = $attr_ref->{'mail_id'};
- $fn =~ m{^[A-Za-z0-9][A-Za-z0-9/_.+-]*\z}s or die "Invalid mail_id '$fn'";
- $msginfo->mail_id($fn);
- if (!exists($attr_ref->{'secret_id'}) || $attr_ref->{'secret_id'} eq '') {
- die "Secret_id is required, but missing" if c('auth_required_release');
- } else {
- my($id) = Digest::MD5->new->add($attr_ref->{'secret_id'})->b64digest;
- $id = substr($id,0,12); $id =~ tr{/}{-};
- $id eq $fn or die "Result $id of secret_id does not match mail_id $fn";
- }
- $quar_type = $attr_ref->{'quar_type'};
- if ($quar_type eq '') # choose some reasonable default (simpleminded)
- { $quar_type = c('spam_quarantine_method') =~ /^sql:/i ? 'Q' : 'F' }
- if ($quar_type eq 'F' || $quar_type eq 'Z') {
- $QUARANTINEDIR ne '' or die "Config variable \$QUARANTINEDIR is empty";
- if ($attr_ref->{'mail_file'} ne '') {
- $fn = $attr_ref->{'mail_file'};
- $fn =~ m{^[A-Za-z0-9][A-Za-z0-9/_.+-]*\z}s && $fn !~ m{\.\./}
- or die "Unsafe filename '$fn'";
- $fn = $QUARANTINEDIR.'/'.untaint($fn);
- } else { # automatically guess a filename - simpleminded
- if ($quarantine_subdir_levels < 1) { $fn = "$QUARANTINEDIR/$fn" }
- else { my($subd) = substr($fn,0,1); $fn = "$QUARANTINEDIR/$subd/$fn" }
- $fn .= '.gz' if $quar_type eq 'Z';
- }
- }
- $msginfo->mail_text_fn($fn);
- } elsif (!exists $attr_ref->{'mail_file'}) {
- $msginfo->mail_text_fn($msginfo->mail_tempdir . '/email.txt');
- } else {
- # SECURITY: just believe the supplied file name, blindly untainting it
- $msginfo->mail_text_fn(untaint($attr_ref->{'mail_file'}));
- }
- if ($ampdp && $msginfo->mail_text_fn ne '') {
- my($fh); my($fname) = $msginfo->mail_text_fn;
- new_am_id('rel-'.$msginfo->mail_id) if $attr_ref->{'request'} eq 'release';
- if ($attr_ref->{'request'} eq 'release' && $quar_type eq 'Q') {
- do_log(5, "preprocess_policy_query: opening in sql: ".$msginfo->mail_id);
- my($obj) = $Amavis::sql_storage;
- $Amavis::extra_code_sql_quar && $obj
- or die "SQL quarantine code not enabled";
- my($conn_h) = $obj->{conn_h}; my($sql_cl_r) = cr('sql_clause');
- $conn_h->begin_work_nontransaction; # (re)connect if not connected
- $fh = Amavis::IO::SQL->new;
- $fh->open($conn_h,$sql_cl_r->{'sel_quar'},untaint($msginfo->mail_id))
- or die "Can't open sql obj for reading: $!";
- } else {
- do_log(5, "preprocess_policy_query: opening mail '$fname'");
- # set new amavis message id
- new_am_id( ($fname =~ m{amavis-(milter-)?([^/ \t]+)}s ? $2 : undef) )
- if $attr_ref->{'request'} ne 'release';
- # file created by amavis helper program or other client, just open it
- my(@stat_list) = lstat($fname); my($errn) = @stat_list ? 0 : 0+$!;
- if ($errn == ENOENT) { die "File $fname does not exist" }
- elsif ($errn) { die "File $fname inaccessible: $!" }
- elsif (!-f _) { die "File $fname is not a plain file" }
- add_entropy(@stat_list);
- if ($fname =~ /\.gz\z/) {
- $fh = Amavis::IO::Zlib->new;
- $fh->open($fname,'rb') or die "Can't open gzipped file $fname: $!";
- } else {
- $msginfo->msg_size(-s _);
- $fh = IO::File->new;
- $fh->open($fname,'<') or die "Can't open file $fname: $!";
- binmode($fh,":bytes") or die "Can't cancel :utf8 mode: $!"
- if $unicode_aware;
- }
- }
- $msginfo->mail_text($fh); # save file handle to object
- }
- if ($ampdp) {
- do_log(1, sprintf("%s %s %s: <%s> -> %s",
- $attr_ref->{'request'}, $attr_ref->{'mail_id'},
- $msginfo->mail_tempdir, $sender,
- join(',', qquote_rfc2821_local(@recips)) ));
- } else {
- do_log(1, sprintf("Request: %s(%s): %s %s %s: %s[%s] <%s> -> <%s>",
- @$attr_ref{qw(request protocol_state mail_id protocol_name
- queue_id client_name client_address sender recipient)}));
- }
- $msginfo;
- }
- sub check_amcl_policy($$$$) {
- my($conn,$msginfo,$check_mail,$old_amcl) = @_;
- my($smtp_resp, $exit_code, $preserve_evidence);
- my(%baseline_policy_bank); my($policy_changed) = 0;
- %baseline_policy_bank = %current_policy_bank;
- # do some sanity checks before deciding to call check_mail()
- if (!ref($msginfo->per_recip_data) || !defined($msginfo->mail_text)) {
- $smtp_resp = '450 4.5.0 Incomplete request'; $exit_code = EX_TEMPFAIL;
- } else {
- my($cl_ip) = $msginfo->client_addr; my($sender) = $msginfo->sender;
- if ($cl_ip ne '' && defined $policy_bank{'MYNETS'}
- && lookup_ip_acl($cl_ip,@{ca('mynetworks_maps')}) ) {
- Amavis::load_policy_bank('MYNETS'); $policy_changed = 1;
- }
- if ($sender ne '' && defined $policy_bank{'MYUSERS'}
- && lookup(0,$sender,@{ca('local_domains_maps')})) {
- Amavis::load_policy_bank('MYUSERS'); $policy_changed = 1;
- }
- debug_oneshot(1) if lookup(0,$sender,@{ca('debug_sender_maps')});
- # check_mail() expects open file on $fh, need not be rewound
- Amavis::check_mail_begin_task();
- ($smtp_resp, $exit_code, $preserve_evidence) =
- &$check_mail($conn,$msginfo,0);
- my($fh) = $msginfo->mail_text; my($tempdir) = $msginfo->mail_tempdir;
- $fh->close or die "Error closing temp file: $!" if $fh;
- $fh = undef; $msginfo->mail_text(undef);
- my($errn) = $tempdir eq '' ? ENOENT : (stat($tempdir) ? 0 : 0+$!);
- if ($tempdir eq '' || $errn == ENOENT) {
- # do nothing
- } elsif ($msginfo->client_delete) {
- do_log(4, "AM.PDP: deletion of $tempdir is client's responsibility");
- } elsif ($preserve_evidence) {
- do_log(-1,"AM.PDP: tempdir is to be PRESERVED: $tempdir");
- } else {
- my($fname) = $msginfo->mail_text_fn;
- do_log(4, "AM.PDP: tempdir and file being removed: $tempdir, $fname");
- unlink($fname) or die "Can't remove file $fname: $!" if $fname ne '';
- rmdir_recursively($tempdir);
- }
- }
- # amavisd -> amavis-helper protocol response consists of any number of
- # the following lines, the response is terminated by an empty line
- # addrcpt=recipient
- # delrcpt=recipient
- # addheader=hdr_head hdr_body
- # chgheader=index hdr_head hdr_body
- # delheader=index hdr_head
- # replacebody=new_body (not implemented)
- # return_value=continue|reject|discard|accept|tempfail
- # setreply=rcode xcode message
- # exit_code=n
- my(@response); my($rcpt_deletes,$rcpt_count)=(0,0);
- if (ref($msginfo->per_recip_data)) {
- for my $r (@{$msginfo->per_recip_data})
- { $rcpt_count++; if ($r->recip_done) { $rcpt_deletes++ } }
- }
- local($1,$2,$3);
- if ($smtp_resp=~/^([1-5]\d\d) ([245]\.\d{1,3}\.\d{1,3})(?: |\z)(.*)\z/s)
- { push(@response, proto_encode('setreply', $1,$2,$3)) }
- if ( $exit_code == EX_TEMPFAIL) {
- push(@response, proto_encode('return_value','tempfail'));
- } elsif ($exit_code == EX_NOUSER) { # reject the whole message
- push(@response, proto_encode('return_value','reject'));
- } elsif ($exit_code == EX_UNAVAILABLE) { # reject the whole message
- push(@response, proto_encode('return_value','reject'));
- } elsif ($exit_code == 99) { # discard the whole message
- push(@response, proto_encode('return_value','discard'));
- } elsif ($msginfo->delivery_method ne '') { # explicit forwarding by server
- $rcpt_count==$rcpt_deletes or die "Not all recips done"; # just in case
- # MTA is relieved of duty to deliver a message, amavisd did the forwarding
- $exit_code = EX_OK; # *** 99 or EX_OK; ??? (doesn't really matter with
- # helper client programs which can't do the delivery)
- push(@response, proto_encode('return_value','continue')); # 'discard' ???
- } elsif ($rcpt_count-$rcpt_deletes <= 0) { # none left, should be discarded
- # discarding could have been requested (?)
- do_log(-1, "WARN: no recips left (forgot to set ".
- "\$forward_method=undef using milter?), $smtp_resp");
- $exit_code = 99;
- push(@response, proto_encode('return_value','discard'));
- } else { # EX_OK
- for my $r (@{$msginfo->per_recip_data}) { # modified recipient addresses?
- my($addr,$newaddr) = ($r->recip_addr, $r->recip_final_addr);
- if ($r->recip_done) { # delete
- push(@response, proto_encode('delrcpt',
- quote_rfc2821_local($addr)));
- } elsif ($newaddr ne $addr) { # modify, e.g. adding extension
- push(@response, proto_encode('delrcpt',
- quote_rfc2821_local($addr)));
- push(@response, proto_encode('addrcpt',
- quote_rfc2821_local($newaddr)));
- }
- }
- my($hdr_edits) = $msginfo->header_edits;
- if ($hdr_edits) { # any added or modified header fields?
- local($1,$2);
- # Inserting. Not posible to specify placement of header fields in milter!
- for my $hf (@{$hdr_edits->{prepend}}, @{$hdr_edits->{append}}) {
- if ($hf =~ /^([^:]+):[ \t]*(.*?)$/s)
- { push(@response, proto_encode('addheader',$1,$2)) }
- }
- my($field_name,$edit,$field_body);
- while ( ($field_name,$edit) = each %{$hdr_edits->{edit}} ) {
- $field_body = $msginfo->mime_entity->head->get($field_name,0);
- if (!defined($field_body)) {
- # such header field does not exist, do nothing
- } elsif (!defined($edit)) { # delete existing header field
- push(@response, proto_encode('delheader',"1",$field_name));
- } else { # edit the first occurrence
- chomp($field_body);
- $field_body = hdr($field_name, &$edit($field_name,$field_body));
- $field_body = $1 if $field_body =~ /^[^:]+:[ \t]*(.*?)$/s;
- push(@response, proto_encode('chgheader', "1",
- $field_name, $field_body));
- }
- }
- }
- if ($old_amcl) { # milter via old amavis helper program
- # warn if there is anything that should be done but MTA is not capable of
- # (or a helper program can not pass the request)
- for (grep { /^(delrcpt|addrcpt)=/ } @response)
- { do_log(-1, "WARN: MTA can't do: $_") }
- if ($rcpt_deletes && $rcpt_count-$rcpt_deletes > 0) {
- do_log(-1, "WARN: ACCEPT THE WHOLE MESSAGE, ".
- "MTA-in can't do selective recips deletion");
- }
- }
- push(@response, proto_encode('return_value','continue'));
- }
- push(@response, proto_encode('exit_code',sprintf("%d",$exit_code)));
- ll(2) && do_log(2, "mail checking ended: ".join("\n",@response));
- if ($policy_changed) {
- %current_policy_bank = %baseline_policy_bank; $policy_changed = 0;
- }
- @response;
- }
- sub postfix_policy($$$) {
- my($conn,$msginfo,$attr_ref) = @_;
- my(@response);
- if ($attr_ref->{'request'} ne 'smtpd_access_policy') {
- die ("unknown 'request' value: " . $attr_ref->{'request'});
- } else {
- @response = 'action=DUNNO';
- }
- @response;
- }
- sub proto_encode($@) {
- my($attribute_name,@strings) = @_; local($1);
- $attribute_name =~ # encode all but alfanumerics, '_' and '-'
- s/([^0-9a-zA-Z_-])/sprintf("%%%02x",ord($1))/eg;
- for (@strings) { # encode % and nonprintables
- s/([^\041-\044\046-\176])/sprintf("%%%02x",ord($1))/eg;
- }
- $attribute_name . '=' . join(' ',@strings);
- }
- sub dispatch_from_quarantine($$) {
- my($conn,$msginfo) = @_;
- eval {
- msg_from_quarantine($conn,$msginfo); # fill message object information
- mail_dispatch($conn,$msginfo,1,1); # re-send the mail
- };
- my($err) = $@; chomp($err);
- if ($@ ne '') { do_log(0, "WARN: dispatch_from_quarantine failed: $err") }
- my(@response);
- for my $r (@{$msginfo->per_recip_data}) {
- local($1,$2,$3); my($smtp_s,$smtp_es,$msg);
- my($resp) = $r->recip_smtp_response;
- if ($err ne '')
- { ($smtp_s,$smtp_es,$msg) = ('450', '4.5.0', "ERROR: $err") }
- elsif ($resp =~ /^([1-5]\d\d) ([245]\.\d{1,3}\.\d{1,3})(?: |\z)(.*)\z/s)
- { ($smtp_s,$smtp_es,$msg) = ($1,$2,$3) }
- else
- { ($smtp_s,$smtp_es,$msg) = ('450', '4.5.0', "Unexpected: $resp") }
- push(@response, proto_encode('setreply',$smtp_s,$smtp_es,$msg));
- }
- @response;
- }
- 1;
- __DATA__
- #
- package Amavis::In::SMTP;
- use strict;
- use re 'taint';
- BEGIN {
- use Exporter ();
- use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.043';
- @ISA = qw(Exporter);
- }
- use Errno qw(ENOENT EACCES);
- use MIME::Base64;
- BEGIN {
- import Amavis::Conf qw(:platform :confvars c cr ca);
- import Amavis::Util qw(ll do_log am_id new_am_id snmp_counters_init
- prolong_timer debug_oneshot sanitize_str
- strip_tempdir rmdir_recursively add_entropy);
- import Amavis::Lookup qw(lookup);
- import Amavis::Lookup::IP qw(lookup_ip_acl);
- import Amavis::Timing qw(section_time);
- import Amavis::rfc2821_2822_Tools;
- import Amavis::In::Message;
- import Amavis::In::Connection;
- }
- sub new($) {
- my($class) = @_;
- my($self) = bless {}, $class;
- $self->{sock} = undef; # SMTP socket
- $self->{proto} = undef; # SMTP / ((ESMTP / LMTP) (A | S | SA)? )
- $self->{pipelining} = undef; # may we buffer responses?
- $self->{smtp_outbuf} = undef; # SMTP responses buffer for PIPELINING
- $self->{fh_pers} = undef; # persistent file handle for email.txt
- $self->{tempdir_persistent} = undef;# temporary directory for check_mail
- $self->{preserve} = undef; # don't delete tempdir on exit
- $self->{tempdir_empty} = 1; # anything of interest in tempdir?
- $self->{session_closed_normally} = undef; # closed properly with QUIT
- $self;
- }
- sub preserve_evidence # try to preserve temporary files etc in case of trouble
- { my($self)=shift; !@_ ? $self->{preserve} : ($self->{preserve}=shift) }
- sub DESTROY {
- my($self) = shift;
- eval { do_log(5,"Amavis::In::SMTP DESTROY called") };
- eval {
- $self->{fh_pers}->close
- or die "Error closing temp file: $!" if $self->{fh_pers};
- $self->{fh_pers} = undef;
- my($errn) = $self->{tempdir_pers} eq '' ? ENOENT
- : (stat($self->{tempdir_pers}) ? 0 : 0+$!);
- if (defined $self->{tempdir_pers} && $errn != ENOENT) {
- # this will not be included in the TIMING report,
- # but it only occurs infrequently and doesn't take that long
- if ($self->preserve_evidence && !$self->{tempdir_empty}) {
- do_log(-1,"SMTP shutdown: tempdir is to be PRESERVED: ".
- $self->{tempdir_pers});
- } else {
- do_log(3, sprintf("SMTP shutdown: %s is being removed: %s%s",
- $self->{tempdir_empty} ? 'empty tempdir' : 'tempdir',
- $self->{tempdir_pers},
- $self->preserve_evidence ? ', nothing to preserve' : ''));
- rmdir_recursively($self->{tempdir_pers});
- }
- }
- if (ref($self->{sock}) && ! $self->{session_closed_normally}) {
- $self->smtp_resp(1,"421 4.3.2 Service shutting down, closing channel");
- }
- };
- if ($@ ne '')
- { my($eval_stat) = $@; eval { do_log(1,"SMTP shutdown: $eval_stat") } }
- }
- sub prepare_tempdir($) {
- my($self) = @_;
- if (! defined $self->{tempdir_pers} ) {
- # invent a name for a temporary directory for this child, and create it
- my($now_iso8601) = iso8601_timestamp(time,1); # or: iso8601_utc_timestamp
- $self->{tempdir_pers} = sprintf("%s/amavis-%s-%05d",
- $TEMPBASE, $now_iso8601, $$);
- }
- my($dname) = $self->{tempdir_pers};
- my(@stat_list) = lstat($dname); my($errn) = @stat_list ? 0 : 0+$!;
- if (!$errn && ! -d _) { # exists, but is not a directory !?
- die "prepare_tempdir: $dname is not a directory!!!";
- } elsif (!$errn) {
- my($dev,$ino) = @stat_list;
- if ($dev != $self->{tempdir_dev} || $ino != $self->{tempdir_ino}) {
- do_log(-1,"prepare_tempdir: $dname is no longer the same directory!!!");
- ($self->{tempdir_dev},$self->{tempdir_ino}) = @stat_list;
- }
- } elsif ($errn == ENOENT) {
- do_log(4,"prepare_tempdir: creating directory $dname");
- mkdir($dname,0750) or die "Can't create directory $dname: $!";
- @stat_list = lstat($dname); add_entropy(@stat_list);
- ($self->{tempdir_dev},$self->{tempdir_ino}) = @stat_list;
- $self->{tempdir_empty} = 1;
- section_time('mkdir tempdir');
- }
- # prepare temporary file for writing (and reading later)
- my($fname) = $dname . '/email.txt';
- @stat_list = lstat($fname); $errn = @stat_list ? 0 : 0+$!;
- if ($errn == ENOENT) { # no file
- do_log(0,"$fname no longer exists, can't re-use it") if $self->{fh_pers};
- $self->{fh_pers} = undef;
- } elsif ($errn) { # some other error
- die "prepare_tempdir: can't access $fname: $!";
- $self->{fh_pers} = undef;
- } elsif (! -f _) { # not a regular file !?
- die "prepare_tempdir: $fname is not a regular file!!!";
- $self->{fh_pers} = undef;
- } elsif ($self->{fh_pers}) {
- my($dev,$ino) = @stat_list;
- if ($dev != $self->{file_dev} || $ino != $self->{file_ino}) {
- # may happen if some user code has replaced the file, e.g. by altermime
- do_log(1,"$fname is no longer the same file, won't re-use it, deleting");
- unlink($fname) or die "Can't remove file $fname: $!";
- $self->{fh_pers} = undef;
- }
- }
- if ($self->{fh_pers}) {
- $self->{fh_pers}->seek(0,0) or die "Can't rewind mail file: $!";
- $self->{fh_pers}->truncate(0) or die "Can't truncate mail file: $!";
- } else {
- do_log(4,"prepare_tempdir: creating file $fname");
- $self->{fh_pers} = IO::File->new($fname,'+>',0640)
- or die "Can't create file $fname: $!";
- @stat_list = lstat($fname); add_entropy(@stat_list);
- ($self->{file_dev}, $self->{file_ino}) = @stat_list;
- section_time('create email.txt');
- }
- }
- sub authenticate($$$) {
- my($state,$auth_mech,$auth_resp) = @_;
- my($result,$newchallenge);
- if ($auth_mech eq 'ANONYMOUS') { # rfc2245
- $result = [$auth_resp,undef];
- } elsif ($auth_mech eq 'PLAIN') { # rfc2595, "user\0authname\0pass"
- if (!defined($auth_resp)) { $newchallenge = '' }
- else { $result = [ (split(/\000/,$auth_resp,-1))[0,2] ] }
- } elsif ($auth_mech eq 'LOGIN' && !defined $state) {
- $newchallenge = 'Username:'; $state = [];
- } elsif ($auth_mech eq 'LOGIN' && @$state==0) {
- push(@$state, $auth_resp); $newchallenge = 'Password:';
- } elsif ($auth_mech eq 'LOGIN' && @$state==1) {
- push(@$state, $auth_resp); $result = $state;
- } # CRAM-MD5:rfc2195, DIGEST-MD5:rfc2831
- ($state,$result,$newchallenge);
- }
- # Accept a SMTP or LMTP connect (which can do any number of transactions)
- # and call content checking for each message received
- #
- sub process_smtp_request($$$$) {
- my($self, $sock, $lmtp, $conn, $check_mail) = @_;
- # $sock: connected socket from Net::Server
- # $lmtp: use LMTP protocol instead of (E)SMTP
- # $conn: information about client connection
- # $check_mail: subroutine ref to be called with file handle
- my($msginfo,$authenticated,$auth_user,$auth_pass);
- $self->{sock} = $sock;
- $self->{pipelining} = 0; # may we buffer responses?
- $self->{smtp_outbuf} = []; # SMTP responses buffer for PIPELINING
- my($myheloname);
- # $myheloname = $myhostname;
- # $myheloname = 'localhost';
- # $myheloname = '[127.0.0.1]';
- $myheloname = '[' . $conn->socket_ip . ']';
- new_am_id(undef, $Amavis::child_invocation_count, undef);
- my($initial_am_id) = 1; my($sender,@recips); my($got_rcpt);
- my($max_recip_size_limit); # maximum of per-recipient message size limits
- my($terminating,$aborting,$eof,$voluntary_exit); my($seq) = 0;
- my(%xforward_args); my(%baseline_policy_bank); my($policy_changed);
- %baseline_policy_bank = %current_policy_bank; $policy_changed = 0;
- $conn->smtp_proto($self->{proto} = $lmtp ? 'LMTP' : 'SMTP');
- # system-wide message size limit, if any
- my($message_size_limit) = c('smtpd_message_size_limit');
- if ($message_size_limit && $message_size_limit < 65536)
- { $message_size_limit = 65536 } # rfc2821 requires at least 64k
- my($smtpd_greeting_banner_tmp) = c('smtpd_greeting_banner');
- $smtpd_greeting_banner_tmp =~
- s{ \$ (?: \{ ([^\}]*) \} | ([a-zA-Z0-9_-]+) ) }
- { { 'helo-name' => $myheloname,
- 'version' => $myversion,
- 'version-id' => $myversion_id,
- 'version-date' => $myversion_date,
- 'product' => $myproduct_name,
- 'protocol' => $lmtp?'LMTP':'ESMTP' }->{lc($1.$2)}
- }egx;
- $self->smtp_resp(1, "220 $smtpd_greeting_banner_tmp");
- $0 = sprintf("amavisd (ch%d-idle)", $Amavis::child_invocation_count);
- Amavis::Timing::go_idle(4);
- local($_); local($/) = "\012"; # input line terminator set to LF
- for (undef $!; defined($_=<$sock>); undef $!) {
- $0 = sprintf("amavisd (ch%d-%s)",
- $Amavis::child_invocation_count, am_id());
- Amavis::Timing::go_busy(5);
- prolong_timer('reading SMTP command');
- { # a block is used as a 'switch' statement - 'last' will exit from it
- my($cmd) = $_;
- do_log(4, $self->{proto} . "< $cmd");
- !/^ \s* ([A-Za-z]+) (?: \s+ (.*?) )? \s* \015\012 \z/xs && do {
- $self->smtp_resp(1,"500 5.5.2 Error: bad syntax", 1, $cmd); last;
- };
- $_ = uc($1); my($args) = $2;
- # (causes holdups in Postfix, it doesn't retry immediately; better set max_use)
- # $Amavis::child_task_count >= $max_requests # exceeded max_requests
- # && /^(?:HELO|EHLO|LHLO|DATA|NOOP)\z/ && do { # pipelining checkpoints
- # # in case of multiple-transaction protocols (e.g. SMTP, LMTP)
- # # we do not like to keep running indefinitely at the MTA's mercy
- # my($msg) = "Closing transmission channel ".
- # "after $Amavis::child_task_count transactions, $_";
- # do_log(2,$msg); $self->smtp_resp(1,"421 4.3.0 ".$msg);
- # $terminating=1; last;
- # };
- /^(?:RSET|DATA|QUIT)\z/ && $args ne '' && do {
- $self->smtp_resp(1,"501 5.5.4 Error: $_ does not accept arguments",
- 1,$cmd);
- last;
- };
- /^RSET\z/ && do { $sender = undef; @recips = (); $got_rcpt = 0;
- $max_recip_size_limit = undef; $msginfo = undef;
- if ($policy_changed) {
- %current_policy_bank = %baseline_policy_bank;
- $policy_changed = 0;
- }
- $self->smtp_resp(0,"250 2.0.0 Ok $_"); last;
- };
- /^NOOP\z/ && do { $self->smtp_resp(1,"250 2.0.0 Ok $_"); last };
- /^QUIT\z/ && do {
- my($smtpd_quit_banner_tmp) = c('smtpd_quit_banner');
- $smtpd_quit_banner_tmp =~
- s{ \$ (?: \{ ([^\}]*) \} | ([a-zA-Z0-9_-]+) ) }
- { { 'helo-name' => $myheloname,
- 'version' => $myversion,
- 'version-id' => $myversion_id,
- 'version-date' => $myversion_date,
- 'product' => $myproduct_name,
- 'protocol' => $lmtp?'LMTP':'ESMTP' }->{lc($1.$2)}
- }egx;
- $self->smtp_resp(1,"221 2.0.0 $smtpd_quit_banner_tmp");
- $terminating=1; last;
- };
- ### !$lmtp && /^HELO\z/ && do { # strict
- /^HELO\z/ && do {
- $sender = undef; @recips = (); $got_rcpt = 0; # implies RSET
- $max_recip_size_limit = undef; $msginfo = undef; # forget previous
- if ($policy_changed)
- { %current_policy_bank = %baseline_policy_bank; $policy_changed = 0 }
- $self->{pipelining} = 0; $self->smtp_resp(0,"250 $myheloname");
- $lmtp = 0; $conn->smtp_proto($self->{proto} = 'SMTP');
- $conn->smtp_helo($args); section_time('SMTP HELO'); last;
- };
- ### (!$lmtp && /^EHLO\z/ || $lmtp && /^LHLO\z/) && do { # strict
- /^(?:EHLO|LHLO)\z/ && do {
- $sender = undef; @recips = (); $got_rcpt = 0; # implies RSET
- $max_recip_size_limit = undef; $msginfo = undef; # forget previous
- if ($policy_changed)
- { %current_policy_bank = %baseline_policy_bank; $policy_changed = 0 }
- $lmtp = /^LHLO\z/ ? 1 : 0;
- $conn->smtp_proto($self->{proto} = $lmtp ? 'LMTP' : 'ESMTP');
- $self->{pipelining} = 1;
- $self->smtp_resp(0,"250 $myheloname\n" . join("\n",
- 'PIPELINING',
- !defined($message_size_limit) ? 'SIZE'
- : sprintf('SIZE %d',$message_size_limit),
- '8BITMIME',
- 'ENHANCEDSTATUSCODES',
- !@{ca('auth_mech_avail')} ? ()
- : join(' ','AUTH',@{ca('auth_mech_avail')}),
- 'XFORWARD NAME ADDR PROTO HELO' ));
- $conn->smtp_helo($args); section_time("SMTP $_");
- last;
- };
- /^XFORWARD\z/ && do { # Postfix extension
- if (defined($sender)) {
- $self->smtp_resp(0,"503 5.5.1 Error: XFORWARD not allowed within transaction", 1, $cmd);
- last;
- }
- my($bad);
- for (split(' ',$args)) {
- if (!/^( [A-Za-z0-9] [A-Za-z0-9-]* ) = ( [\041-\176]{0,255} )\z/xs) {
- $self->smtp_resp(0,"501 5.5.4 Syntax error in XFORWARD parameters",
- 1, $cmd);
- $bad = 1; last;
- } else {
- my($name,$val) = (uc($1), $2);
- if ($name =~ /^(?:NAME|ADDR|PROTO|HELO)\z/) {
- $val = undef if uc($val) eq '[UNAVAILABLE]';
- $xforward_args{$name} = $val;
- } else {
- $self->smtp_resp(0,"501 5.5.4 XFORWARD command parameter error: $name=$val",1,$cmd);
- $bad = 1; last;
- }
- }
- }
- $self->smtp_resp(1,"250 2.5.0 Ok $_") if !$bad;
- last;
- };
- /^HELP\z/ && do {
- $self->smtp_resp(1,"214 2.0.0 See amavisd-new home page at:\n".
- "http://www.ijs.si/software/amavisd/");
- last;
- };
- /^AUTH\z/ && @{ca('auth_mech_avail')} && do { # rfc2554
- if ($args !~ /^([^ ]+)(?: ([^ ]*))?\z/is) {
- $self->smtp_resp(0,"501 5.5.2 Syntax: AUTH mech [initresp]",1,$cmd);
- last;
- }
- my($auth_mech,$auth_resp) = (uc($1), $2);
- if ($authenticated) {
- $self->smtp_resp(0,"503 5.5.1 Error: session already authenticated", 1, $cmd);
- } elsif (defined($sender)) {
- $self->smtp_resp(0,"503 5.5.1 Error: AUTH not allowed within transaction", 1, $cmd);
- } elsif (!grep {uc($_) eq $auth_mech} @{ca('auth_mech_avail')}) {
- $self->smtp_resp(0,"504 5.7.6 Error: requested authentication mechanism not supported", 1, $cmd);
- } else {
- my($state,$result,$challenge);
- if ($auth_resp eq '=') { $auth_resp = '' } # zero length
- elsif ($auth_resp eq '') { $auth_resp = undef }
- for (;;) {
- if ($auth_resp !~ m{^[A-Za-z0-9+/=]*\z}) {
- $self->smtp_resp(0,"501 5.5.4 Authentication failed: malformed authentication response", 1, $cmd);
- last;
- } else {
- $auth_resp = decode_base64($auth_resp) if $auth_resp ne '';
- ($state,$result,$challenge) =
- authenticate($state, $auth_mech, $auth_resp);
- if (ref($result) eq 'ARRAY') {
- $self->smtp_resp(0,"235 2.7.1 Authentication successful");
- $authenticated = 1; ($auth_user,$auth_pass) = @$result;
- do_log(2,"AUTH $auth_mech, user=$auth_user");
- # do_log(2,"AUTH $auth_mech, user=$auth_user, pass=$auth_resp");
- last;
- } elsif (defined $result && !$result) {
- $self->smtp_resp(0,"535 5.7.1 Authentication failed", 1, $cmd);
- last;
- }
- }
- # server challenge or ready prompt
- $self->smtp_resp(1,"334 ".encode_base64($challenge,''));
- undef $!; $auth_resp = <$sock>;
- defined $auth_resp || $!==0 or die "Error reading auth resp: $!";
- do_log(5, $self->{proto} . "< $auth_resp");
- $auth_resp =~ s/\015?\012\z//;
- if ($auth_resp eq '*') {
- $self->smtp_resp(0,"501 5.7.1 Authentication aborted");
- last;
- }
- }
- }
- last;
- };
- /^VRFY\z/ && do {
- $self->smtp_resp(1,"502 5.5.1 Command $_ not implemented", 1, $cmd);
- # if ($args eq '') {
- # $self->smtp_resp(1,"501 5.5.2 Syntax: VRFY address", 1, $cmd);
- # } else {
- # $self->smtp_resp(1,"252 2.0.0 Cannot VRFY user, but will accept ".
- # "message and attempt delivery", 0, $cmd);
- # }
- last;
- };
- /^MAIL\z/ && do { # begin new SMTP transaction
- if (defined($sender)) {
- $self->smtp_resp(0,"503 5.5.1 Error: nested MAIL command", 1, $cmd);
- last;
- }
- if (!$authenticated &&
- c('auth_required_inp') && @{ca('auth_mech_avail')} ) {
- $self->smtp_resp(0,"530 5.7.1 Authentication required", 1, $cmd);
- last;
- }
- # begin SMTP transaction
- my($now) = time;
- prolong_timer('MAIL FROM received - timer reset', $child_timeout);
- if (!$seq) { # the first connect
- section_time('SMTP pre-MAIL');
- } else { # establish new time reference for each transaction
- Amavis::Timing::init(); snmp_counters_init();
- }
- $seq++;
- new_am_id(undef,$Amavis::child_invocation_count,$seq)
- if !$initial_am_id;
- $initial_am_id = 0;
- Amavis::check_mail_begin_task();
- $self->prepare_tempdir;
- my($cl_ip) = $xforward_args{'ADDR'};
- if ($cl_ip ne '' && defined $policy_bank{'MYNETS'}
- && lookup_ip_acl($cl_ip,@{ca('mynetworks_maps')}) ) {
- Amavis::load_policy_bank('MYNETS'); $policy_changed = 1;
- }
- $msginfo = Amavis::In::Message->new;
- $msginfo->rx_time($now);
- # $msginfo->body_type('7bit'); # presumed, unless explicitly declared
- $msginfo->delivery_method(c('forward_method'));
- my($submitter);
- if ($authenticated) {
- $msginfo->auth_user($auth_user); $msginfo->auth_pass($auth_pass);
- $conn->smtp_proto($self->{proto}.'A') # rfc3848
- if $self->{proto} =~ /^(LMTP|ESMTP)\z/i;
- } elsif (c('auth_reauthenticate_forwarded') &&
- c('amavis_auth_user') ne '') {
- $msginfo->auth_user(c('amavis_auth_user'));
- $msginfo->auth_pass(c('amavis_auth_pass'));
- $submitter = quote_rfc2821_local(c('mailfrom_notify_recip'));
- }
- $msginfo->client_addr($xforward_args{'ADDR'});
- $msginfo->client_name($xforward_args{'NAME'});
- $msginfo->client_proto($xforward_args{'PROTO'});
- $msginfo->client_helo($xforward_args{'HELO'});
- %xforward_args = (); # reset values for the next transaction
- # permit some sloppy syntax without angle brackets
- if ($args !~ /^FROM: \s*
- ( < (?: " (?: \\. | [^\\"] )* " | [^"@] )*
- (?: @ (?: \[ (?: \\. | [^\]\\] )* \] |
- [^\[\]\\>] )* )?
- > |
- [^<\s] (?: " (?: \\. | [^\\"] )* " | [^"\s] )*
- ) (?: \s+ ([\040-\176]+) )? \z/isx ) {
- $self->smtp_resp(0,"501 5.5.2 Syntax: MAIL FROM: <address>",1,$cmd);
- last;
- }
- my($bad); my($addr,$opt) = ($1,$2);
- for (split(' ',$opt)) {
- if (!/^ ( [A-Za-z0-9] [A-Za-z0-9-]* ) =
- ( [\041-\074\076-\176]+ ) \z/xs) { # printable, not '=' or SP
- $self->smtp_resp(0,"501 5.5.4 Syntax error in MAIL FROM parameters",
- 1,$cmd);
- $bad = 1; last;
- } else {
- my($name,$val) = (uc($1),$2);
- if ($name eq 'SIZE' && $val=~/^\d{1,20}\z/) { # rfc1870
- $msginfo->msg_size($val+0);
- if ($message_size_limit && $val > $message_size_limit) {
- my($msg) = "552 5.3.4 Declared message size ($val B) ".
- "exceeds fixed size limit";
- do_log(0, $self->{proto}." REJECT 'MAIL FROM': $msg");
- $self->smtp_resp(0,$msg, 0,$cmd);
- $bad = 1; last;
- }
- } elsif ($name eq 'BODY' && $val=~/^(?:7BIT|8BITMIME)\z/i){
- $msginfo->body_type(uc($val));
- } elsif ($name eq 'AUTH' && @{ca('auth_mech_avail')} &&
- !defined($submitter) ) { # rfc2554
- $submitter = $val; # encoded as xtext: rfc3461
- $submitter =~ s/\+([0-9a-fA-F]{2})/pack("C",hex($1))/eg;
- do_log(5, "MAIL command, $authenticated, submitter: $submitter");
- } else {
- my($msg);
- if ($name eq 'AUTH' && !@{ca('auth_mech_avail')}) {
- $msg = "503 5.7.4 Error: authentication disabled";
- } else {
- $msg = "504 5.5.4 MAIL command parameter error: $name=$val";
- }
- $self->smtp_resp(0,$msg,1,$cmd);
- $bad = 1; last;
- }
- }
- }
- if (!$bad) {
- $addr = ($addr =~ /^<(.*)>\z/s) ? $1 : $addr;
- $self->smtp_resp(0,"250 2.1.0 Sender $addr OK");
- $sender = unquote_rfc2821_local($addr);
- if ($sender ne '' && defined $policy_bank{'MYUSERS'}
- && lookup(0,$sender,@{ca('local_domains_maps')})) {
- Amavis::load_policy_bank('MYUSERS'); $policy_changed = 1;
- }
- debug_oneshot(lookup(0,$sender,@{ca('debug_sender_maps')}) ? 1 : 0,
- $self->{proto} . "< $cmd");
- # $submitter = $addr if !defined($submitter); # rfc2554: MAY
- $submitter = '<>' if !defined($msginfo->auth_user);
- $msginfo->auth_submitter($submitter);
- };
- last;
- };
- /^RCPT\z/ && do {
- if (!defined($sender)) {
- $self->smtp_resp(0,"503 5.5.1 Need MAIL command before RCPT",1,$cmd);
- @recips = (); $got_rcpt = 0;
- last;
- }
- $got_rcpt++;
- # permit some sloppy syntax without angle brackets
- if ($args !~ /^TO: \s*
- ( < (?: " (?: \\. | [^\\"] )* " | [^"@] )*
- (?: @ (?: \[ (?: \\. | [^\]\\] )* \] |
- [^\[\]\\>] )* )?
- > |
- [^<\s] (?: " (?: \\. | [^\\"] )* " | [^"\s] )*
- ) (?: \s+ ([\040-\176]+) )? \z/isx ) {
- $self->smtp_resp(0,"501 5.5.2 Syntax: RCPT TO: <address>",1,$cmd);
- last;
- }
- if ($2 ne '') {
- $self->smtp_resp(0,"504 5.5.4 RCPT command parameter not implemented: $2",
- 1, $cmd);
- ### $self->smtp_resp(0,"555 5.5.4 RCPT command parameter unrecognized: $2", 1, $cmd);
- } elsif ($got_rcpt > $smtpd_recipient_limit) {
- $self->smtp_resp(0,"452 4.5.3 Too many recipients");
- } else {
- my($addr,$opt) = ($1, $2);
- $addr = ($addr =~ /^<(.*)>\z/s) ? $1 : $addr;
- my($addr_unq) = unquote_rfc2821_local($addr);
- my($recip_size_limit); my($mslm) = ca('message_size_limit_maps');
- $recip_size_limit = lookup(0,$addr_unq, @$mslm) if @$mslm;
- if ($recip_size_limit && $recip_size_limit < 65536)
- { $recip_size_limit = 65536 } # rfc2821 requires at least 64k
- if ($recip_size_limit > $max_recip_size_limit)
- { $max_recip_size_limit = $recip_size_limit }
- my($mail_size) = $msginfo->msg_size;
- if (defined $mail_size && $recip_size_limit && $mail_size > $recip_size_limit) {
- my($msg) = "552 5.3.4 Declared message size ($mail_size B) ".
- "exceeds recipient's size limit <$addr>";
- do_log(0, $self->{proto}." REJECT 'RCPT TO': $msg");
- $self->smtp_resp(0,$msg, 0,$cmd);
- } else {
- push(@recips,$addr_unq);
- $self->smtp_resp(0,"250 2.1.5 Recipient $addr OK");
- my ($user, $domain) = split('@', $addr);
- if (defined $recipient_policy_bank_map{$addr}) {
- Amavis::load_policy_bank($recipient_policy_bank_map{$addr});
- do_log(1, sprintf("Policy bank '%s' taken for recp '%s'",
- $recipient_policy_bank_map{$addr},
- $addr));
- } elsif (defined $recipient_policy_bank_map{$domain}) {
- Amavis::load_policy_bank($recipient_policy_bank_map{$domain});
- do_log(1, sprintf("Policy bank '%s' taken for recp '%s'",
- $recipient_policy_bank_map{$domain},
- $addr));
- }
- foreach my $recipient_re (keys(%recipient_policy_bank_re_map)) {
- if ($addr =~ /$recipient_re/) {
- Amavis::load_policy_bank($recipient_policy_bank_re_map{$recipient_re});
- do_log(1, sprintf("Policy bank '%s' taken for recp '%s'",
- $recipient_policy_bank_re_map{$domain},
- $addr));
- }
- }
- }
- };
- last;
- };
- /^DATA\z/ && !@recips && do {
- if (!defined($sender)) {
- $self->smtp_resp(1,"503 5.5.1 Need MAIL command before DATA",1,$cmd);
- } elsif (!$got_rcpt) {
- $self->smtp_resp(1,"503 5.5.1 Need RCPT command before DATA",1,$cmd);
- } elsif ($lmtp) { # rfc2033 requires 503 code!
- $self->smtp_resp(1,"503 5.1.1 Error (DATA): no valid recipients",0,$cmd);
- } else {
- $self->smtp_resp(1,"554 5.1.1 Error (DATA): no valid recipients",0,$cmd);
- }
- last;
- };
- /^DATA\z/ && do {
- # set timer to the initial value, MTA timer starts here
- prolong_timer('DATA received - timer reset', $child_timeout);
- if ($message_size_limit) { # enforce system-wide size limit
- if (!$max_recip_size_limit ||
- $max_recip_size_limit > $message_size_limit) {
- $max_recip_size_limit = $message_size_limit;
- }
- }
- my($within_data_transfer,$complete);
- my($size) = 0; my($over_size) = 0;
- eval {
- $msginfo->sender($sender); $msginfo->recips(\@recips);
- ll(1) && do_log(1, sprintf("%s:%s:%s %s: <%s> -> %s Received: %s",
- $conn->smtp_proto,
- $conn->socket_ip eq $inet_socket_bind ? ''
- : '['.$conn->socket_ip.']',
- $conn->socket_port, $self->{tempdir_pers},
- $sender, join(',', qquote_rfc2821_local(@recips)),
- join(' ', ($msginfo->msg_size eq '' ? ()
- : 'SIZE='.$msginfo->msg_size),
- ($msginfo->body_type eq '' ? ()
- : 'BODY='.$msginfo->body_type),
- received_line($conn,$msginfo,am_id(),0) )
- ) );
- $self->smtp_resp(1,"354 End data with <CR><LF>.<CR><LF>");
- $within_data_transfer = 1;
- section_time('SMTP pre-DATA-flush') if $self->{pipelining};
- $self->{tempdir_empty} = 0;
- if ($max_recip_size_limit == 0) { # no message size limit enforced
- my($ln); local($/) = "\015\012"; # input line terminator CRLF
- # credativ -jw
- my $in_headers = 1;
- my $got_received = 0;
- # credativ end
- for ($!=0; defined($ln=<$sock>); $!=0) { # optimized for speed
- if ($ln =~ /^\./) {
- if ($ln eq ".\015\012")
- { $complete = 1; $within_data_transfer = 0; last }
- $ln =~ s/^\.(.+\015\012)\z/$1/s; # dot de-stuffing, rfc2821
- }
- $size += length($ln); # message size is defined in rfc1870
- # credativ -jw
- if (!$got_received && $in_headers && $ln =~ /^Received:/) {
- my $header = $ln;
- # the header might be broken up in different
- # ways according to the length of the
- # strings
- $header =~ tr/\n/ /;
- $header =~ tr/\t/ /;
- $header =~ tr/\r/ /;
- $header =~ s/ / /g;
- $header =~ s/^([^;]+;).*/$1/;
- if ($header =~ /\(Postfix\) with E?SMTP id ([A-Z0-9]+)(;| for)/) {
- $msginfo->postfixid($1);
- } elsif ($header =~ /\(Postfix, from userid \d+\) id ([A-Z0-9]+);/) {
- $msginfo->postfixid($1);
- }
- $got_received = 1;
- }
- if (/^$/m) {
- $in_headers = 0;
- }
- # credativ end
- # remove \015\012: s/// slowest, chomp faster, substr(,0,-2) best
- print {$self->{fh_pers}} substr($ln,0,-2),$eol
- or die "Can't write to mail file: $!";
- }
- defined $ln || $!==0 or die "Connection broken during DATA: $!";
- } else { # enforce size limit
- do_log(5,"enforcing size limit $max_recip_size_limit during DATA");
- my($ln); local($/) = "\015\012"; # input line terminator CRLF
- for ($!=0; defined($ln=<$sock>); $!=0) {
- # do_log(5, $self->{proto} . "< $ln");
- if ($ln =~ /^\./) {
- if ($ln eq ".\015\012")
- { $complete = 1; $within_data_transfer = 0; last }
- $ln =~ s/^\.(.+\015\012)\z/$1/s; # dot de-stuffing, rfc2821
- }
- $size += length($ln); # message size is defined in rfc1870
- if (!$over_size) {
- print {$self->{fh_pers}} substr($ln,0,-2),$eol
- or die "Can't write to mail file: $!";
- if ($max_recip_size_limit && $size > $max_recip_size_limit) {
- do_log(1,"Message size exceeded $max_recip_size_limit B, ".
- "skiping further input");
- print {$self->{fh_pers}} $eol,"***TRUNCATED***",$eol
- or die "Can't write to mail file: $!";
- $over_size = 1;
- }
- }
- }
- defined $ln || $!==0 or die "Connection broken during DATA: $!";
- }; # restores line terminator
- $eof = 1 if !$complete;
- # normal data termination, or eof on socket, or fatal error
- do_log(4, $self->{proto} . "< .\015\012") if $complete;
- $self->{fh_pers}->flush or die "Can't flush mail file: $!";
- # On some systems you have to do a seek whenever you
- # switch between reading and writing. Amongst other things,
- # this may have the effect of calling stdio's clearerr(3).
- # credativ -jw
- my $size = $self->{fh_pers}->tell();
- do_log(0, "original postfix id: ". $msginfo->postfixid . ", size: " . $size);
- # XXX - nrcpts
- # credativ end
- $self->{fh_pers}->seek(0,1) or die "Can't seek on file: $!";
- section_time('SMTP DATA');
- }; # end eval
- if ($@ ne '' || !$complete || $over_size) { # err or connection broken
- chomp($@);
- # on error, either send: '421 Shutting down',
- # or: '451 Aborted, error in processing' and NOT shut down!
- if ($over_size && $@ eq '' && !$within_data_transfer) {
- my($msg) = "552 5.3.4 Message size ($size B) exceeds size limit";
- do_log(0, $self->{proto}." REJECT: $msg");
- $self->smtp_resp(0,$msg, 0,$cmd);
- } elsif (!$within_data_transfer) {
- my($msg) = "Error in processing: " .
- !$complete && $@ eq '' ? 'incomplete' : $@;
- do_log(-2, $self->{proto}." TROUBLE: 451 4.5.0 $msg");
- $self->smtp_resp(1, "451 4.5.0 $msg");
- ### $aborting = $msg;
- } else {
- $aborting = "Connection broken during data transfer" if $eof;
- $aborting .= ', ' if $aborting ne '' && $@ ne '';
- $aborting .= $@;
- $aborting = '???' if $aborting eq '';
- do_log($@ ne '' ? -1 : 3, $self->{proto}." ABORTING: ".$aborting);
- }
- } else { # all OK
- #
- # Is it acceptable to do all this processing here,
- # before returning response??? According to rfc1047
- # it is not a good idea! But at the moment we do not have
- # much choice, amavis has no queueing mechanism and can not
- # accept responsibility for delivery.
- #
- # check contents before responding
- # check_mail() expects open file on $self->{fh_pers},
- # need not be rewound
- $msginfo->mail_tempdir($self->{tempdir_pers});
- $msginfo->mail_text_fn($self->{tempdir_pers} . '/email.txt');
- $msginfo->mail_text($self->{fh_pers});
- my($declared_size) = $msginfo->msg_size;
- if (!defined($declared_size)) {
- } elsif ($size > $declared_size) { # shouldn't happen with decent MTA
- do_log(2,"Actual message size $size B greater than the ".
- "declared $declared_size B");
- } elsif ($size < $declared_size) { # not unusual, but permitted
- do_log(4,"Actual message size $size B, declared $declared_size B");
- }
- $msginfo->msg_size($size); # store actual mail size
- my($smtp_resp, $exit_code, $preserve_evidence) =
- &$check_mail($conn,$msginfo,$lmtp);
- alarm(0); # stop the timer
- if ($preserve_evidence) { $self->preserve_evidence(1) }
- if ($smtp_resp !~ /^4/ &&
- grep { !$_->recip_done } @{$msginfo->per_recip_data}) {
- if ($msginfo->delivery_method eq '') {
- do_log(2,"not all recipients done, forward_method is empty");
- } else {
- die "TROUBLE: (MISCONFIG) not all recipients done, " .
- "forward_method is: " . $msginfo->delivery_method;
- }
- }
- if (!$lmtp) {
- do_log(4, "sending SMTP response: \"$smtp_resp\"");
- $self->smtp_resp(0, $smtp_resp);
- } else {
- my($bounced) = $msginfo->dsn_sent;
- for my $r (@{$msginfo->per_recip_data}) {
- my($resp) = $r->recip_smtp_response;
- if ($bounced && $smtp_resp=~/^2/ && $resp!~/^2/) {
- # as the message was already bounced by us,
- # MTA must not bounce it again; failure status
- # needs to be converted into success!
- $resp = sprintf("250 2.5.0 Ok %s, DSN %s (%s)",
- $r->recip_addr, $bounced==1 ? 'sent' : 'muted', $resp);
- }
- do_log(4, sprintf("sending LMTP response for <%s>: \"%s\"",
- $r->recip_addr, $resp));
- $self->smtp_resp(0, $resp);
- }
- }
- };
- alarm(0); do_log(5,"timer stopped after DATA end");
- if ($self->preserve_evidence && !$self->{tempdir_empty}) {
- # keep evidence in case of trouble
- do_log(-1,"PRESERVING EVIDENCE in ".$self->{tempdir_pers});
- $self->{fh_pers}->close or die "Error closing mail file: $!";
- $self->{fh_pers} = undef; $self->{tempdir_pers} = undef;
- $self->{tempdir_empty} = 1;
- }
- # cleanup, but leave directory (and file handle if possible) for reuse
- if ($self->{fh_pers} && !$can_truncate) {
- # truncate is not standard across all Unix variants,
- # it is not Posix, but is XPG4-UNIX.
- # So if we can't truncate a file and leave it open,
- # we have to create it anew later, at some cost.
- #
- $self->{fh_pers}->close or die "Error closing mail file: $!";
- $self->{fh_pers} = undef;
- unlink($self->{tempdir_pers}.'/email.txt')
- or die "Can't delete file ".$self->{tempdir_pers}."/email.txt: $!";
- section_time('delete email.txt');
- }
- if (defined $self->{tempdir_pers}) { # prepare for the next one
- strip_tempdir($self->{tempdir_pers}); $self->{tempdir_empty} = 1;
- }
- $sender = undef; @recips = (); $got_rcpt = 0; # implicit RSET
- $max_recip_size_limit = undef; $msginfo = undef; # forget previous
- if ($policy_changed)
- { %current_policy_bank = %baseline_policy_bank; $policy_changed = 0 }
- $self->preserve_evidence(0); # reset
- # report elapsed times by section for each transaction
- # (the time for the QUIT remains unaccounted for)
- do_log(2, Amavis::Timing::report());
- Amavis::Timing::init(); snmp_counters_init();
- last;
- }; # DATA
- # catchall (EXPN, TURN, unknown):
- $self->smtp_resp(1,"502 5.5.1 Error: command ($_) not implemented",1,$cmd);
- # $self->smtp_resp(1,"500 5.5.2 Error: command ($_) not recognized", 1,$cmd);
- }; # end of 'switch' block
- if ($terminating || defined $aborting) { # exit SMTP-session loop
- $voluntary_exit = 1; last;
- }
- # rfc2920 requires a flush whenever the local TCP input buffer is
- # emptied. Since we can't check it (unless we use sysread & select),
- # we should do a flush here to be in compliance.
- $self->smtp_resp_flush;
- $0 = sprintf("amavisd (ch%d-%s-idle)",
- $Amavis::child_invocation_count, am_id());
- Amavis::Timing::go_idle(6);
- } # end of loop
- my($errn,$errs);
- if (!$voluntary_exit) {
- $eof = 1;
- if (!defined($_)) { $errn = 0+$!; $errs = "$!" }
- }
- $0 = sprintf("amavisd (ch%d)", $Amavis::child_invocation_count);
- Amavis::Timing::go_busy(7);
- # come here when: QUIT is received, eof or err on socket, or we need to abort
- $self->smtp_resp_flush; # just in case, the session might have been disconnected
- my($msg) =
- defined $aborting && !$eof ? "ABORTING the session: $aborting" :
- defined $aborting ? $aborting :
- !$terminating ? "client broke the connection without a QUIT ($errs)" : '';
- do_log($aborting?-1:3, $self->{proto}.': NOTICE: '.$msg) if $msg ne '';
- if (defined $aborting && !$eof)
- { $self->smtp_resp(1,"421 4.3.2 Service shutting down, ".$aborting) }
- $self->{session_closed_normally} = 1;
- # closes connection after child_finish_hook
- }
- # sends a SMTP response consisting of 3-digit code and an optional message;
- # slow down evil clients by delaying response on permanent errors
- sub smtp_resp($$$;$$) {
- my($self, $flush,$resp, $penalize,$line) = @_;
- if ($penalize) {
- do_log(-1, $self->{proto} . ": $resp; PENALIZE: $line");
- sleep 5;
- section_time('SMTP penalty wait');
- }
- $resp = sanitize_str($resp,1);
- local($1,$2,$3,$4);
- if ($resp !~ /^ ([1-5]\d\d) (\ |-|\z)
- ([245] \. \d{1,3} \. \d{1,3} (?: \ |\z) )?
- (.*) \z/xs)
- { die "Internal error(2): bad SMTP response code: '$resp'" }
- my($resp_code,$continuation,$enhanced,$tail) = ($1,$2,$3,$4);
- $enhanced = '' if !defined($enhanced); # avoids a warning
- my($lead_len) = length($resp_code) + 1 + length($enhanced);
- while (length($tail) > 512-2-$lead_len || $tail =~ /\n/) {
- # rfc2821: The maximum total length of a reply line including the
- # reply code and the <CRLF> is 512 characters. More information
- # may be conveyed through multiple-line replies.
- my($head) = substr($tail,0,512-2-$lead_len);
- if ($head =~ /^([^\n]*\n)/) { $head = $1 }
- $tail = substr($tail,length($head)); chomp($head);
- push(@{$self->{smtp_outbuf}}, $resp_code.'-'.$enhanced.$head);
- }
- push(@{$self->{smtp_outbuf}}, $resp_code.$continuation.$enhanced.$tail);
- $self->smtp_resp_flush if $flush || !$self->{pipelining} ||
- @{$self->{smtp_outbuf}} > 200;
- }
- sub smtp_resp_flush($) {
- my($self) = shift;
- if (ref($self->{smtp_outbuf}) && @{$self->{smtp_outbuf}}) {
- if (ll(4)) {
- for my $resp (@{$self->{smtp_outbuf}})
- { do_log(4, $self->{proto} . "> $resp") };
- }
- my($stat) =
- $self->{sock}->print(map { $_."\015\012" } @{$self->{smtp_outbuf}} );
- @{$self->{smtp_outbuf}} = (); # prevent printing again even if error
- $stat or die "Error writing a SMTP response to the socket: $!";
- }
- }
- 1;
- __DATA__
- #
- package Amavis::AV;
- use strict;
- use re 'taint';
- BEGIN {
- use Exporter ();
- use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.043';
- @ISA = qw(Exporter);
- }
- use POSIX qw(WIFEXITED WIFSIGNALED WIFSTOPPED
- WEXITSTATUS WTERMSIG WSTOPSIG);
- use Errno qw(EPIPE ENOTCONN ENOENT EACCES EAGAIN ECONNRESET);
- use Socket;
- use IO::Socket;
- use IO::Socket::UNIX;
- use subs @EXPORT_OK;
- use vars @EXPORT;
- BEGIN {
- import Amavis::Conf qw(:platform :confvars c cr ca);
- import Amavis::Util qw(ll untaint min max do_log am_id rmdir_recursively
- exit_status_str run_command);
- import Amavis::Timing qw(section_time);
- }
- use vars qw(%st_socket_created %st_sock); # keep persistent state (per-socket)
- # subroutine available for calling from @av_scanners list entries;
- # it has the same args and returns as run_av() below
- sub ask_daemon { ask_av(\&ask_daemon_internal, @_) }
- sub clamav_module_init($) {
- my($av_name) = @_;
- # each child should reinitialize clamav module to reload databases.
- my($clamav_version) = Mail::ClamAV->VERSION;
- my($dbdir) = Mail::ClamAV::retdbdir();
- my($clamav_obj) = Mail::ClamAV->new($dbdir);
- ref $clamav_obj
- or die "$av_name: Can't load db from $dbdir: $Mail::ClamAV::Error";
- $clamav_obj->buildtrie;
- $clamav_obj->maxreclevel($MAXLEVELS) if $MAXLEVELS;
- $clamav_obj->maxfiles($MAXFILES);
- $clamav_obj->maxfilesize($MAX_EXPANSION_QUOTA || 30*1024*1024);
- if ($clamav_version >= 0.12) {
- $clamav_obj->maxratio($MAX_EXPANSION_FACTOR);
- # $clamav_obj->archivememlim(0); # limit memory usage for bzip2 (0/1)
- }
- do_log(2,"$av_name init");
- section_time('clamav_module_init');
- ($clamav_obj,$clamav_version);
- }
- # to be called from sub ask_clamav
- use vars qw($clamav_obj $clamav_version);
- sub clamav_module_internal($@) {
- my($query, $bare_fnames,$names_to_parts,$tempdir, $av_name) = @_;
- if (!defined $clamav_obj) {
- ($clamav_obj,$clamav_version) = clamav_module_init($av_name); # first time
- } elsif ($clamav_obj->statchkdir) { # db reload needed?
- do_log(2, "$av_name: reloading virus database");
- ($clamav_obj,$clamav_version) = clamav_module_init($av_name);
- }
- my($fname) = "$tempdir/parts/$query"; # file to be checked
- my($part) = $names_to_parts->{$query}; # get corresponding parts object
- my($options) = 0; # bitfield of options to Mail::ClamAV::scan
- my($opt_archive,$opt_mail);
- if ($clamav_version < 0.12) {
- $opt_archive = &Mail::ClamAV::CL_ARCHIVE;
- $opt_mail = &Mail::ClamAV::CL_MAIL;
- } else { # >= 0.12, reflects renamed flags in libclamav 0.80
- $opt_archive = &Mail::ClamAV::CL_SCAN_ARCHIVE;
- $opt_mail = &Mail::ClamAV::CL_SCAN_MAIL;
- }
- $options |= &Mail::ClamAV::CL_SCAN_STDOPT if $clamav_version >= 0.13;
- $options |= $opt_archive; # turn on ARCHIVE
- $options &= ~$opt_mail; # turn off MAIL
- if (ref($part) && (lc($part->type_short) eq 'mail' ||
- lc($part->type_declared) eq 'message/rfc822')) {
- do_log(2, "$av_name: $query - enabling option CL_MAIL");
- $options |= $opt_mail; # turn on MAIL
- }
- my($ret) = $clamav_obj->scan(untaint($fname), $options);
- my($output,$status);
- if ($ret->virus) { $status = 1; $output = "INFECTED: $ret" }
- elsif ($ret->clean) { $status = 0; $output = "CLEAN" }
- else { $status = 2; $output = $ret->error.", errno=".$ret->errno }
- ($status,$output); # return synthesised status and a result string
- }
- # subroutine available for calling from @av_scanners list entries;
- # it has the same args and returns as run_av() below
- sub ask_clamav { ask_av(\&clamav_module_internal, @_) }
- my($savi_obj);
- sub sophos_savi_init {
- my($av_name, $command) = @_;
- my(@savi_bool_options) = qw(
- GrpArchiveUnpack GrpSelfExtract GrpExecutable GrpInternet GrpMSOffice
- GrpMisc !GrpDisinfect !GrpClean
- EnableAutoStop FullSweep FullPdf Xml
- );
- $savi_obj = SAVI->new;
- ref $savi_obj or die "$av_name: Can't create SAVI object, err=$savi_obj";
- my($status) = $savi_obj->load_data;
- !defined($status) or die "$av_name: Failed to load SAVI virus data " .
- $savi_obj->error_string($status) . " ($status)";
- my($version) = $savi_obj->version;
- ref $version or die "$av_name: Can't get SAVI version, err=$version";
- do_log(2,sprintf("$av_name init: Version %s (engine %d.%d) ".
- "recognizing %d viruses", $version->string,
- $version->major, $version->minor, $version->count));
- my($error);
- if ($MAXLEVELS) {
- $error = $savi_obj->set('MaxRecursionDepth', $MAXLEVELS);
- !defined $error
- or die "$av_name: error setting MaxRecursionDepth: err=$error";
- }
- $error = $savi_obj->set('NamespaceSupport', 3); # new with Sophos 3.67
- !defined $error
- or do_log(-1,"$av_name: error setting NamespaceSupport: err=$error");
- for (@savi_bool_options) {
- my($value) = /^!/ ? 0 : 1; s/^!+//;
- $error = $savi_obj->set($_, $value);
- !defined $error or die "$av_name: Error setting $_: err=$error";
- }
- section_time('sophos_savi_init');
- 1;
- }
- sub sophos_savi_stale {
- defined $savi_obj && $savi_obj->stale;
- }
- sub sophos_savi_reload {
- if (defined $savi_obj) {
- my($status) = $savi_obj->load_data();
- !defined($status) or die "Failed to load SAVI virus data " .
- $savi_obj->error_string($status) . " ($status)";
- my($version) = $savi_obj->version;
- ref $version or die "Can't get SAVI version, err=$version";
- do_log(2,sprintf("Updated SAVI data: Version %s (engine %d.%d) ".
- "recognizing %d viruses", $version->string,
- $version->major, $version->minor, $version->count));
- }
- }
- # to be called from sub sophos_savi
- sub sophos_savi_internal {
- my($query,
- $bare_fnames,$names_to_parts,$tempdir, $av_name,$command,$args) = @_;
- my($fname) = "$tempdir/parts/$query"; # file to be checked
- if (!c('bypass_decode_parts')) {
- my($part) = $names_to_parts->{$query}; # get corresponding parts object
- my($mime_option_value) = 0;
- if (ref($part) && (lc($part->type_short) eq 'mail' ||
- lc($part->type_declared) eq 'message/rfc822')) {
- do_log(2, "$av_name: $query - enabling option Mime");
- $mime_option_value = 1;
- }
- my($error) = $savi_obj->set('Mime', $mime_option_value);
- !defined $error or die sprintf("%s: Error %s option Mime: err=%s",
- $av_name, $mime_option_value ? 'setting' : 'clearing', $error);
- }
- my($output,$status); my($result) = $savi_obj->scan($fname);
- if (!ref($result)) { # error
- my($msg) = "error scanning file $fname, " .
- $savi_obj->error_string($result) . " ($result) $!";
- if (! grep {$result == $_} (514,527,530,538,549) ) {
- $status = 2; $output = "ERROR $query: $msg";
- } else { # don't panic on non-fatal (encrypted, corrupted, partial)
- $status = 0; $output = "CLEAN $query: $msg";
- }
- do_log(5,"$av_name: $output");
- } elsif ($result->infected) {
- $status = 1; $output = join(", ", $result->viruses) . " FOUND";
- } else {
- $status = 0; $output = "CLEAN $query";
- }
- ($status,$output); # return synthesised status and a result string
- }
- # subroutine available for calling from @av_scanners list entries;
- # it has the same args and returns as run_av() below
- sub ask_sophos_savi {
- my($bare_fnames,$names_to_parts,$tempdir, $av_name,$command,$args,
- $sts_clean,$sts_infected,$how_to_get_names) = @_;
- if (@_ < 3+6) { # supply default arguments for backwards compatibility
- $args = ["*"]; $sts_clean = [0]; $sts_infected = [1];
- $how_to_get_names = qr/^(.*) FOUND$/;
- }
- ask_av(\&sophos_savi_internal,
- $bare_fnames,$names_to_parts,$tempdir, $av_name,$command,$args,
- $sts_clean, $sts_infected, $how_to_get_names);
- }
- # same args and returns as run_av() below,
- # but prepended by a $query, which is the string to be sent to the daemon.
- # Handles both UNIX and INET domain sockets.
- # More than one socket may be specified for redundancy, they will be tried
- # one after the other until one succeeds.
- #
- sub ask_daemon_internal {
- my($query, # expanded query template, often a command and a file or dir name
- $bare_fnames,$names_to_parts,$tempdir, $av_name,$command,$args,
- $sts_clean,$sts_infected,$how_to_get_names, # regexps
- ) = @_;
- my($query_template_orig,$sockets) = @$args;
- my($output) = ''; my($socketname,$is_inet);
- if (!ref($sockets)) { $sockets = [ $sockets ] }
- my($max_retries) = 2 * @$sockets; my($retries) = 0;
- $SIG{PIPE} = 'IGNORE'; # 'send' to broken pipe would throw a signal
- # Sophie and Trophie can accept multiple requests per session
- # and return a single line response each time
- my($multisession) = $av_name =~ /^(Sophie|Trophie)/i ? 1 : 0;
- for (;;) { # gracefully handle cases when av child times out or restarts
- @$sockets >= 1 or die "no sockets specified!?"; # sanity
- $socketname = $sockets->[0]; # try the first one in the current list
- $is_inet = $socketname =~ m{^/} ? 0 : 1; # simpleminded: unix vs. inet sock
- eval {
- if (!$st_socket_created{$socketname}) {
- ll(3) && do_log(3, "$av_name: Connecting to socket " .
- join(' ',$daemon_chroot_dir,$socketname).
- (!$retries ? '' : ", retry #$retries") );
- if ($is_inet) { # inet socket
- $st_sock{$socketname} = IO::Socket::INET->new($socketname)
- or die "Can't connect to INET socket $socketname: $!\n";
- $st_socket_created{$socketname} = 1;
- } else { # unix socket
- $st_sock{$socketname} = IO::Socket::UNIX->new(Type => SOCK_STREAM)
- or die "Can't create UNIX socket: $!\n";
- $st_socket_created{$socketname} = 1;
- $st_sock{$socketname}->connect( pack_sockaddr_un($socketname) )
- or die "Can't connect to UNIX socket $socketname: $!\n";
- }
- }
- ll(3) && do_log(3,sprintf("$av_name: Sending %s to %s socket %s",
- $query, $is_inet?"INET":"UNIX", $socketname));
- # UGLY: bypass send method in IO::Socket to be able to retrieve
- # status/errno directly from 'send', not from 'getpeername':
- defined send($st_sock{$socketname}, $query, 0)
- or die "Can't send to socket $socketname: $!\n";
- my($rv); my($buff) = ''; undef $!;
- while (defined($rv = $st_sock{$socketname}->recv($buff,8192,0))) {
- $output .= $buff;
- last if $multisession || $buff eq '';
- undef $!;
- }
- defined $rv || $!==0 || $!==ECONNRESET
- or die "Error receiving from $socketname: $!\n";
- if (!$multisession) {
- $st_sock{$socketname}->close
- or die "Error closing socket $socketname: $!\n";
- $st_sock{$socketname} = undef; $st_socket_created{$socketname} = 0;
- }
- $! = undef;
- $output ne '' or die "Empty result from $socketname\n";
- };
- last if $@ eq '';
- # error handling (most interesting error codes are EPIPE and ENOTCONN)
- chomp($@); my($err) = "$!"; my($errn) = 0+$!;
- ++$retries <= $max_retries
- or die "Too many retries to talk to $socketname ($@)";
- # is ECONNREFUSED for INET sockets common enough too?
- if ($retries <= 1 && $errn == EPIPE) { # common, don't cause concern
- do_log(2,"$av_name broken pipe (don't worry), retrying ($retries)");
- } else {
- do_log( ($retries>1?-1:1), "$av_name: $@, retrying ($retries)");
- if ($retries % @$sockets == 0) { # every time the list is exhausted
- my($dly) = min(20, 1 + 5 * ($retries/@$sockets - 1));
- do_log(3,"$av_name: sleeping for $dly s");
- sleep($dly); # slow down a possible runaway
- }
- }
- if ($st_socket_created{$socketname}) {
- # prepare for a retry, ignore 'close' status
- $st_sock{$socketname}->close;
- $st_sock{$socketname} = undef; $st_socket_created{$socketname} = 0;
- }
- # leave good socket as the first entry in the list
- # so that it will be tried first when needed again
- push(@$sockets, shift @$sockets) if @$sockets>1; # circular shift left
- }
- (0,$output); # return synthesised status and result string
- }
- # ask_av is a common subroutine available to be used by ask_daemon, ask_clamav,
- # ask_sophos_savi and similar front-end routines used in @av_scanners entries.
- # It traverses supplied files or directory ($bare_fnames) and calls a supplied
- # subroutine for each file to be scanned, summarizing the final av scan result.
- # It has the same args and returns as run_av() below, prepended by a checking
- # subroutine argument.
- sub ask_av {
- my($code) = shift; # strip away the first argument, a subroutine ref
- my($bare_fnames,$names_to_parts,$tempdir, $av_name,$command,$args,
- $sts_clean,$sts_infected,$how_to_get_names) = @_;
- my($query_template) = ref $args eq 'ARRAY' ? $args->[0] : $args;
- do_log(5, "ask_av ($av_name): query template1: $query_template");
- my($checking_each_file) = $query_template =~ /\*/;
- my($scan_status,@virusname); my($output) = '';
- for my $f ($checking_each_file ? @$bare_fnames : ("$tempdir/parts")) {
- my($query) = $query_template;
- if (!$checking_each_file) { # scanner can be given a directory name
- $query =~ s[{}][$tempdir/parts]g; # replace {} with directory name
- do_log(3,"Using ($av_name) on dir: $query");
- } else { # must check each file individually
- # replace {}/* with directory name and file, and * with current file name
- $query =~ s[ ({}/)? \* ]
- [ !defined($1) || $1 eq '' ? $f : "$tempdir/parts/$f" ]gesx;
- do_log(3,"Using ($av_name) on file: $query");
- }
- my($t_status,$t_output) = &$code($query, @_);
- do_log(4,"ask_av ($av_name) result: $t_output");
- # braindead Perl: ""=~/x{0}/ serves as explicit default for an empty regexp
- if (defined $sts_infected && (
- ref($sts_infected) eq 'ARRAY' ? (grep {$_==$t_status} @$sts_infected)
- : ""=~/x{0}/ && $t_output=~/$sts_infected/m)) { # is infected
- # test for infected first, in case both expressions match
- $scan_status = 1; # 'true' indicates virus found, no errors
- my(@t_virusnames) = ref($how_to_get_names) eq 'CODE'
- ? &$how_to_get_names($t_output)
- : ""=~/x{0}/ && $t_output=~/$how_to_get_names/gm;
- @t_virusnames = map { defined $_ ? $_ : () } @t_virusnames;
- push(@virusname, @t_virusnames);
- $output .= $t_output . $eol;
- do_log(2,"ask_av ($av_name): $f INFECTED: ".join(", ",@t_virusnames));
- } elsif (!defined($sts_clean)) { # clean, but inconclusive
- # by convention: undef $sts_clean means result is inconclusive,
- # file appears clean, but continue scanning with other av scanners,
- # the current scanner does not want to vouch for it; useful for a
- # scanner like jpeg checker which tests for one vulnerability only
- do_log(3,"ask_av ($av_name): $f CLEAN, but inconclusive");
- } elsif (ref($sts_clean) eq 'ARRAY'
- ? (grep {$_==$t_status} @$sts_clean)
- : ""=~/x{0}/ && $t_output=~/$sts_clean/m) { # is clean
- $scan_status = 0 if !$scan_status; # no viruses, no errors
- do_log(3,"ask_av ($av_name): $f CLEAN");
- } else {
- do_log(-2,"ask_av ($av_name) FAILED - unexpected result: $t_output");
- last; # error, bail out
- }
- }
- if (!@$bare_fnames) { $scan_status = 0 } # no errors, no viruses
- do_log(3,"$av_name result: clean") if defined($scan_status) && !$scan_status;
- ($scan_status,$output,\@virusname);
- }
- # Call a virus scanner and parse its output.
- # Returns a triplet (or die in case of failure).
- # The first element of the triplet is interpreted as follows:
- # - true if virus found,
- # - 0 if no viruses found,
- # - undef if it did not complete its job;
- # the second element is a string, the text as provided by the virus scanner;
- # the third element is ref to a list of virus names found (if any).
- # (it is guaranteed the list will be nonempty if virus was found)
- #
- sub run_av {
- # first three args are prepended, not part of n-tuple
- my($bare_fnames, # a ref to a list of filenames to scan (basenames)
- $names_to_parts, # ref to a hash that maps base file names to parts object
- $tempdir, # temporary directory
- $av_name, $command, $args,
- $sts_clean, # a ref to a list of status values, or a regexp
- $sts_infected, # a ref to a list of status values, or a regexp
- $how_to_get_names, # ref to sub, or a regexp to get list of virus names
- $pre_code, $post_code, # routines to be invoked before and after av
- ) = @_;
- my($scan_status,$virusnames,$error_str); my($output) = '';
- &$pre_code(@_) if defined $pre_code;
- if (ref($command) eq 'CODE') {
- do_log(3,"Using $av_name: (built-in interface)");
- ($scan_status,$output,$virusnames) = &$command(@_);
- } else {
- local($1); my(@args) = split(' ',$args);
- if (grep { m{^({}/)?\*\z} } @args) { # {}/* or *, list each file
- # replace asterisks with bare file names (basenames) if alone or in {}/*
- @args = map { !m{^({}/)?\*\z} ? $_
- : map {$1.untaint($_)} @$bare_fnames } @args;
- }
- for (@args) { s[{}][$tempdir/parts]g } # replace {} with directory name
- # NOTE: RAV does not like '</dev/null' in its command!
- ll(3) && do_log(3, "Using ($av_name): " . join(' ',$command,@args));
- my($proc_fh,$pid) = run_command(undef, "&1", $command, @args);
- my($nbytes,$buff);
- while (($nbytes=$proc_fh->read($buff,4096)) > 0) { $output .= $buff }
- defined $nbytes or die "Error reading: $!";
- my($err); $proc_fh->close or $err=$!; my($child_stat) = $?;
- $error_str = exit_status_str($child_stat,$err);
- my($retval) = WEXITSTATUS($child_stat);
- chomp($output); my($output_trimmed) = $output;
- $output_trimmed =~ s/\r\n/\n/gs;
- $output_trimmed =~ s/([ \t\n\r])[ \t\n\r]{4,}/$1.../gs;
- $output_trimmed = "..." . substr($output_trimmed,-800)
- if length($output_trimmed) > 800;
- do_log(3, "run_av: $command $error_str, $output_trimmed");
- # braindead Perl: ""=~/x{0}/ serves as explicit default for an empty regexp
- if (!WIFEXITED($child_stat)) {
- } elsif (defined $sts_infected && (
- ref($sts_infected) eq 'ARRAY'
- ? (grep {$_==$retval} @$sts_infected)
- : ""=~/x{0}/ && $output=~/$sts_infected/m)) { # is infected
- # test for infected first, in case both expressions match
- $virusnames = []; # get a list of virus names by parsing output
- @$virusnames = ref($how_to_get_names) eq 'CODE'
- ? &$how_to_get_names($output)
- : ""=~/x{0}/ && $output=~/$how_to_get_names/gm;
- @$virusnames = map { defined $_ ? $_ : () } @$virusnames;
- $scan_status = 1; # 'true' indicates virus found
- do_log(2,"run_av ($av_name): INFECTED: ".join(", ",@$virusnames));
- } elsif (!defined($sts_clean)) { # clean, but inconclusive
- # by convention: undef $sts_clean means result is inconclusive,
- # file appears clean, but continue scanning with other av scanners,
- # the current scanner does not want to vouch for it; useful for a
- # scanner like jpeg checker which tests for one vulnerability only
- do_log(3,"run_av ($av_name): clean, but inconclusive");
- } elsif (ref($sts_clean) eq 'ARRAY' ? (grep {$_==$retval} @$sts_clean)
- : ""=~/x{0}/ && $output=~/$sts_clean/m) { # is clean
- $scan_status = 0; # 'false' (but defined) indicates no viruses
- do_log(3,"run_av ($av_name): CLEAN");
- } else {
- $error_str = "unexpected $error_str, output=\"$output_trimmed\"";
- do_log(-2,"run_av ($av_name) FAILED - ".$error_str);
- }
- $output = $output_trimmed if length($output) > 900;
- }
- &$post_code(@_) if defined $post_code;
- $virusnames = [] if !defined $virusnames;
- @$virusnames = (undef) if $scan_status && !@$virusnames; # nonnil
- if (!defined($scan_status) && defined($error_str)) {
- die "$command $error_str"; # die is more informative than return value
- }
- ($scan_status, $output, $virusnames);
- }
- sub virus_scan($$$) {
- my($tempdir,$firsttime,$parts_root) = @_;
- my($scan_status,$output,@virusname,@detecting_scanners);
- my($anyone_done); my($anyone_tried);
- my($bare_fnames_ref,$names_to_parts);
- my(@errors); my($j); my($tier) = 'primary';
- for my $av (@{ca('av_scanners')}, "\000", @{ca('av_scanners_backup')}) {
- next if !defined $av;
- if ($av eq "\000") { # 'magic' separator between lists
- last if $anyone_done;
- do_log(-2,"WARN: all $tier virus scanners failed, considering backups");
- $tier = 'secondary'; next;
- }
- next if !ref $av || !defined $av->[1];
- if (!defined $bare_fnames_ref) { # first time: collect file names to scan
- ($bare_fnames_ref,$names_to_parts) =
- files_to_scan("$tempdir/parts",$parts_root);
- do_log(2, "Not calling virus scanners, ".
- "no files to scan in $tempdir/parts") if !@$bare_fnames_ref;
- }
- $anyone_tried++; my($this_status,$this_output,$this_vn);
- if (!@$bare_fnames_ref) { # no files to scan?
- ($this_status,$this_output,$this_vn) = (0, '', []); # declare clean
- } else { # call virus scanner
- eval {
- ($this_status,$this_output,$this_vn) =
- run_av($bare_fnames_ref,$names_to_parts,$tempdir, @$av);
- };
- if ($@ ne '') {
- my($err) = $@; chomp($err);
- $err = "$av->[0] av-scanner FAILED: $err";
- do_log(-2,$err); push(@errors,$err);
- $this_status = undef;
- };
- }
- $anyone_done++ if defined $this_status;
- $j++; section_time("AV-scan-$j");
- if ($this_status) { # virus detected by this scanner
- push(@detecting_scanners, $av->[0]);
- if (!@virusname) { # store results of the first scanner detecting
- @virusname = @$this_vn;
- $scan_status = $this_status; $output = $this_output;
- }
- last if c('first_infected_stops_scan'); # stop now if we found a virus?
- } elsif (!defined($scan_status)) { # tentatively keep regardless of status
- $scan_status = $this_status; $output = $this_output;
- }
- }
- if (@virusname && @detecting_scanners) {
- my(@ds) = @detecting_scanners; for (@ds) { s/,/;/ } # facilitates parsing
- ll(2) && do_log(2, sprintf("virus_scan: (%s), detected by %d scanners: %s",
- join(', ',@virusname), scalar(@ds), join(', ',@ds)));
- }
- $output =~ s{\Q$tempdir\E/parts/?}{}gs if defined $output; # hide path info
- if (!$anyone_tried) { die "NO VIRUS SCANNERS AVAILABLE\n" }
- elsif (!$anyone_done)
- { die ("ALL VIRUS SCANNERS FAILED: ".join("; ",@errors)."\n") }
- ($scan_status, $output, \@virusname, \@detecting_scanners); # return a quad
- }
- # return a ref to a list of files to be scanned in a given directory
- sub files_to_scan($$) {
- my($dir,$parts_root) = @_;
- my($names_to_parts) = {}; # a hash that maps base file names
- # to Amavis::Unpackers::Part object
- # traverse decomposed parts tree breadth-first, match it to actual files
- for (my($part), my(@unvisited)=($parts_root);
- @unvisited and $part=shift(@unvisited);
- push(@unvisited,@{$part->children}))
- { $names_to_parts->{$part->base_name} = $part if $part ne $parts_root }
- my($bare_fnames_ref) = []; my(%bare_fnames);
- local(*DIR); opendir(DIR,$dir) or die "Can't open directory $dir: $!";
- my(@dirfiles) = readdir(DIR); # must avoid modifying dir. while traversing it
- closedir(DIR) or die "Error closing directory $dir: $!";
- # traverse parts directory and check for actual files
- for my $f (@dirfiles) {
- my($fname) = "$dir/$f";
- my($errn) = lstat($fname) ? 0 : 0+$!;
- next if $errn == ENOENT;
- if ($errn) { die "files_to_scan: file $fname inaccessible: $!" }
- if (!-r _) { # attempting to gain read access to the file
- do_log(3,"files_to_scan: attempting to gain read access to $fname");
- chmod(0750,untaint($fname))
- or die "files_to_scan: Can't change protection on $fname: $!";
- $errn = lstat($fname) ? 0 : 0+$!;
- if ($errn) { die "files_to_scan: file $fname inaccessible: $!" }
- if (!-r _) { die "files_to_scan: file $fname not readable" }
- }
- next if ($f eq '.' || $f eq '..') && -d _; # this or the parent directory
- if (!-f _ || !exists $names_to_parts->{$f}) { # nonregular f. or unexpected
- my($what) = -l _ ? 'symlink' : -d _ ? 'directory' : -f _ ? 'file'
- : 'non-regular file';
- my($msg) = "removing unexpected $what $fname";
- $msg .= ", it has no corresponding parts object"
- if !exists $names_to_parts->{$f};
- do_log(-1, "WARN: files_to_scan: ".$msg);
- if (-d _) { rmdir_recursively(untaint($fname)) }
- else { unlink(untaint($fname)) or die "Can't delete $what $fname: $!" }
- } elsif (-z _) {
- # empty file
- } else {
- if ($f !~ /^[A-Za-z0-9_.-]+\z/s)
- {do_log(-1,"WARN: files_to_scan: unexpected/suspicious file name: $f")}
- push(@$bare_fnames_ref,$f); $bare_fnames{$f} = 1;
- }
- }
- # remove entries from %$names_to_parts that have no corresponding files
- my($fname,$part);
- while ( ($fname,$part) = each %$names_to_parts ) {
- next if exists $bare_fnames{$fname};
- if (ll(4) && $part->exists) {
- my($type_short) = $part->type_short;
- do_log(4,sprintf("files_to_scan: info: part %s (%s) no longer present",
- $fname, (!ref $type_short ? $type_short : join(', ',@$type_short)) ));
- }
- delete $names_to_parts->{$fname}; # delete is allowed for the current elem.
- }
- ($bare_fnames_ref, $names_to_parts);
- }
- 1;
- __DATA__
- #
- package Amavis::SpamControl;
- use strict;
- use re 'taint';
- BEGIN {
- use Exporter ();
- use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.043';
- @ISA = qw(Exporter);
- }
- use Errno qw(EAGAIN);
- use FileHandle;
- use POSIX ();
- use Mail::SpamAssassin;
- BEGIN {
- import Amavis::Conf qw(:platform :sa $daemon_user c cr ca);
- import Amavis::Util qw(ll do_log exit_status_str run_command
- prolong_timer add_entropy);
- import Amavis::rfc2821_2822_Tools;
- import Amavis::Timing qw(section_time);
- import Amavis::Lookup qw(lookup);
- }
- use subs @EXPORT_OK;
- use vars qw($spamassassin_obj);
- # called at startup, before the main fork
- sub init() {
- do_log(1, "SpamControl: initializing Mail::SpamAssassin");
- my($saved_umask) = umask;
- $spamassassin_obj = Mail::SpamAssassin->new({
- debug => $sa_debug,
- save_pattern_hits => $sa_debug,
- dont_copy_prefs => 1,
- local_tests_only => $sa_local_tests_only,
- home_dir_for_helpers => $helpers_home,
- stop_at_threshold => 0,
- site_rules_filename => $sa_site_rules_filename,
- # DEF_RULES_DIR => '/usr/local/share/spamassassin',
- # LOCAL_RULES_DIR => '/etc/mail/spamassassin',
- #see man Mail::SpamAssassin for other options
- });
- # $Mail::SpamAssassin::DEBUG->{rbl}=-3;
- # $Mail::SpamAssassin::DEBUG->{dcc}=-3;
- # $Mail::SpamAssassin::DEBUG->{pyzor}=-3;
- # $Mail::SpamAssassin::DEBUG->{bayes}=-3;
- # $Mail::SpamAssassin::DEBUG->{rulesrun}=4+64;
- my($sa_version) = Mail::SpamAssassin::Version();
- if ($sa_auto_whitelist && $sa_version=~/^(\d+(?:\.\d+)?)/ && $1 < 3) {
- do_log(1, "SpamControl: turning on SA auto-whitelisting (AWL)");
- # create a factory for the persistent address list
- my($addrlstfactory) = Mail::SpamAssassin::DBBasedAddrList->new;
- $spamassassin_obj->set_persistent_address_list_factory($addrlstfactory);
- }
- $spamassassin_obj->compile_now; # try to ensure modules are preloaded
- alarm(0); # seems like SA forgets to clear alarm in some cases
- umask($saved_umask); # restore our umask, SA clobbered it
- do_log(1, "SpamControl: done");
- }
- # check envelope sender if white or blacklisted by each recipient;
- # Saves the result in recip_blacklisted_sender and recip_whitelisted_sender
- # properties of each recipient object.
- #
- sub white_black_list($$$$$) {
- my($conn,$msginfo,$sql_wblist,$user_id_sql,$ldap_policy) = @_;
- my($any_w)=0; my($any_b)=0; my($all)=1; my($wr,$br);
- my($sender) = $msginfo->sender;
- do_log(4,"wbl: checking sender <$sender>");
- for my $r (@{$msginfo->per_recip_data}) {
- next if $r->recip_done; # already dealt with
- my($found,$wb,$boost); my($recip) = $r->recip_addr;
- my($user_id_ref,$mk_ref) = !defined $sql_wblist ? ([],[])
- : lookup(1,$recip,$user_id_sql);
- do_log(5,"wbl: (SQL) recip <$recip>, ".scalar(@$user_id_ref)." matches")
- if defined $sql_wblist && ll(5);
- for my $ind (0..$#{$user_id_ref}) { # for ALL SQL sets matching the recip
- my($user_id) = $user_id_ref->[$ind]; my($mkey);
- ($wb,$mkey) = lookup(0,$sender,
- Amavis::Lookup::SQLfield->new($sql_wblist,'wb','S',$user_id) );
- do_log(4,"wbl: (SQL) recip <$recip>, rid=$user_id, got: \"$wb\"");
- if (!defined($wb)) { # NULL field or no match: remains undefined
- } elsif ($wb =~ /^ *([+-]?\d+(?:\.\d*)?) *\z/) { # numeric
- my($val) = 0+$1; # penalty points to be added to the score
- $boost += $val;
- ll(2) && do_log(2,sprintf(
- "wbl: (SQL) soft-%slisted (%s) sender <%s> => <%s> (rid=%s)",
- ($val<0?'white':'black'), $val, $sender, $recip, $user_id));
- $wb = undef; # not hard- white or blacklisting
- } elsif ($wb =~ /^[ \000]*\z/) { # neutral, stops the search
- $found++; $wb = 0;
- do_log(5,"wbl: (SQL) recip <$recip> is neutral to sender <$sender>");
- } elsif ($wb =~ /^([BbNnFf])[ ]*\z/) { # blacklisted (B, N, F)
- $found++; $wb = -1; $any_b++; $br = $recip;
- $r->recip_blacklisted_sender(1);
- do_log(5,"wbl: (SQL) recip <$recip> blacklisted sender <$sender>");
- } else { # whitelisted (W, Y, T) or anything else
- if ($wb =~ /^([WwYyTt])[ ]*\z/) {
- do_log(5, "wbl: (SQL) recip <$recip> whitelisted sender <$sender>");
- } else {
- do_log(-1,"wbl: (SQL) recip <$recip> whitelisted sender <$sender>, ".
- "unexpected wb field value: \"$wb\"");
- }
- $found++; $wb = +1; $any_w++; $wr = $recip;
- $r->recip_whitelisted_sender(1);
- }
- last if $found;
- }
- if (!$found && defined($ldap_policy)) {
- my($wblist);
- my($keys_ref,$rhs_ref) = make_query_keys($sender,0,0);
- my(@keys) = @$keys_ref;
- unshift(@keys, '<>') if $sender eq ''; # a hack for a null return path
- $_ = Amavis::Util::untaint($_) for @keys; # untaint keys
- $_ = Net::LDAP::Util::escape_filter_value($_) for @keys;
- do_log(5,sprintf("wbl: (LDAP) query keys: %s",
- join(', ',map{"\"$_\""}@keys)));
- $wblist = lookup(0,$recip,Amavis::Lookup::LDAPattr->new(
- $ldap_policy,'amavisBlacklistSender','L-'));
- for my $key (@keys) {
- if (grep {/^\Q$key\E\z/i} @$wblist) {
- $found++; $wb = -1; $br = $recip; $any_b++;
- $r->recip_blacklisted_sender(1);
- do_log(5,"wbl: (LDAP) recip <$recip> blacklisted sender <$sender>");
- }
- }
- $wblist = lookup(0,$recip,Amavis::Lookup::LDAPattr->new(
- $ldap_policy,'amavisWhitelistSender','L-'));
- for my $key (@keys) {
- if (grep {/^\Q$key\E\z/i} @$wblist) {
- $found++; $wb = +1; $wr = $recip; $any_w++;
- $r->recip_whitelisted_sender(1);
- do_log(5,"wbl: (LDAP) recip <$recip> whitelisted sender <$sender>");
- }
- }
- }
- if (!$found) { # fall back to static lookups if no match
- # sender can be both white- and blacklisted at the same time
- my($val); my($r_ref,$mk_ref,@t);
- # NOTE on the specifics of $per_recip_blacklist_sender_lookup_tables :
- # the $r_ref below is supposed to be a ref to a single lookup table
- # for compatibility with pre-2.0 versions of amavisd-new;
- # Note that this is different from @score_sender_maps, which is
- # supposed to contain a ref to a _list_ of lookup tables as a result
- # of the first-level lookup (on the recipient address as a key).
- #
- ($r_ref,$mk_ref) = lookup(0,$recip,
- Amavis::Lookup::Label->new("blacklist_recip<$recip>"),
- cr('per_recip_blacklist_sender_lookup_tables'));
- @t = ( (defined $r_ref ? $r_ref : ()), @{ca('blacklist_sender_maps')} );
- $val = lookup(0,$sender,
- Amavis::Lookup::Label->new("blacklist_sender<$sender>"),
- @t) if @t;
- if ($val) {
- $found++; $wb = -1; $br = $recip; $any_b++;
- $r->recip_blacklisted_sender(1);
- do_log(5,"wbl: recip <$recip> blacklisted sender <$sender>");
- }
- # similar for whitelists:
- ($r_ref,$mk_ref) = lookup(0,$recip,
- Amavis::Lookup::Label->new("whitelist_recip<$recip>"),
- cr('per_recip_whitelist_sender_lookup_tables'));
- @t = ( (defined $r_ref ? $r_ref : ()), @{ca('whitelist_sender_maps')} );
- $val = lookup(0,$sender,
- Amavis::Lookup::Label->new("whitelist_sender<$sender>"),
- @t) if @t;
- if ($val) {
- $found++; $wb = +1; $wr = $recip; $any_w++;
- $r->recip_whitelisted_sender(1);
- do_log(5,"wbl: recip <$recip> whitelisted sender <$sender>");
- }
- }
- if (!defined($boost)) { # static lookups if no match
- # note the first argument of lookup() is true, requesting ALL matches
- my($r_ref,$mk_ref) = lookup(1,$recip,
- Amavis::Lookup::Label->new("score_recip<$recip>"),
- @{ca('score_sender_maps')});
- for my $j (0..$#{$r_ref}) { # for ALL tables matching the recipient
- my($val,$key) = lookup(0,$sender,
- Amavis::Lookup::Label->new("score_sender<$sender>"),
- @{$r_ref->[$j]} );
- if (defined $val && $val != 0) {
- $boost += $val;
- ll(2) && do_log(2,
- sprintf("wbl: soft-%slisted (%s) sender <%s> => <%s>, ".
- "recip_key=\"%s\"", ($val<0?'white':'black'),
- $val, $sender, $recip, $mk_ref->[$j]));
- }
- }
- }
- $r->recip_score_boost($boost) if defined $boost;
- $all = 0 if !$wb;
- }
- if (!ll(2)) {
- # don't bother preparing log report which will not be printed
- } else {
- my($msg) = '';
- if ($all && $any_w && !$any_b) { $msg = "whitelisted" }
- elsif ($all && $any_b && !$any_w) { $msg = "blacklisted" }
- elsif ($all) { $msg = "black or whitelisted by all recips" }
- elsif ($any_b || $any_w) {
- $msg .= "whitelisted by ".($any_w>1?"$any_w recips, ":"$wr, ") if $any_w;
- $msg .= "blacklisted by ".($any_b>1?"$any_b recips, ":"$br, ") if $any_b;
- $msg .= "but not by all,";
- }
- do_log(2,"wbl: $msg sender <$sender>") if $msg ne '';
- }
- ($any_w+$any_b, $all);
- }
- # - returns true if spam detected,
- # - returns 0 if no spam found,
- # - throws exception (die) in case of errors,
- # or just returns undef if it did not complete its jobs
- #
- sub spam_scan($$) {
- my($conn,$msginfo) = @_;
- my($spam_level,$spam_status,$spam_report,$autolearn_status); my(@lines);
- my($hdr_edits) = $msginfo->header_edits;
- if (!$hdr_edits) {
- $hdr_edits = Amavis::Out::EditHeader->new;
- $msginfo->header_edits($hdr_edits);
- }
- my($dspam_signature,$dspam_result,$dspam_fname);
- push(@lines, sprintf("Return-Path: %s\n", # fake a local delivery agent
- qquote_rfc2821_local($msginfo->sender)));
- push(@lines, sprintf("X-Envelope-To: %s\n",
- join(",\n ",qquote_rfc2821_local(@{$msginfo->recips}))));
- my($fh) = $msginfo->mail_text;
- my($mbsl) = c('sa_mail_body_size_limit');
- if ( defined $mbsl &&
- ($msginfo->orig_body_size > $mbsl ||
- $msginfo->msg_size > 5*1024 + $mbsl)
- ) {
- do_log(1,"spam_scan: not wasting time on SA, message ".
- "longer than $mbsl bytes: ".
- $msginfo->orig_header_size .'+'. $msginfo->orig_body_size);
- } else {
- if (!defined($dspam) || $dspam eq '') {
- do_log(5,"spam_scan: DSPAM not available, skipping it");
- } else {
- # pass the mail to DSPAM, extract its result headers and feed them to SA
- $dspam_fname = $msginfo->mail_tempdir . '/dspam.msg';
- my($dspam_fh) = IO::File->new; # will receive output from DSPAM
- $dspam_fh->open($dspam_fname, O_CREAT|O_EXCL|O_WRONLY, 0640)
- or die "Can't create file $dspam_fname: $!";
- $fh->seek(0,0) or die "Can't rewind mail file: $!";
- my($proc_fh,$pid) = run_command('&'.fileno($fh), "&1", $dspam,
- qw(--stdout --deliver=spam,innocent
- --mode=tum --feature=chained,noise
- --enable-signature-headers
- --user), $daemon_user,
- ); # --mode=teft
- # qw(--stdout --deliver-spam) # dspam < 3.0
- # keep X-DSPAM-*, ignore other changes e.g. Content-Transfer-Encoding
- my($all_local) = !grep { !lookup(0,$_,@{ca('local_domains_maps')}) }
- @{$msginfo->recips};
- my($first_line); my($ln);
- # scan mail header from DSPAM
- for (undef $!; defined($ln=$proc_fh->getline); undef $!) {
- $dspam_fh->print($ln) or die "Can't write to $dspam_fname: $!";
- if (!defined($first_line))
- { $first_line = $ln; do_log(5,"spam_scan: from DSPAM: $first_line") }
- last if $ln eq $eol;
- local($1,$2);
- if ($ln =~ /^(X-DSPAM[^:]*):[ \t]*(.*)$/) { # does not handle folding
- my($hh,$hb) = ($1,$2);
- $dspam_signature = $hb if $ln =~ /^X-DSPAM-Signature:/i;
- $dspam_result = $hb if $ln =~ /^X-DSPAM-Result:/i;
- do_log(3,$ln); push(@lines,$ln); # store header in array passed to SA
- # add DSPAM header fields to passed mail for all recipients
- $hdr_edits->append_header($hh,$hb) if $all_local;
- }
- }
- defined $ln || $!==0 || $!==EAGAIN
- or die "Error reading from DSPAM process: $!";
- my($nbytes,$buff);
- while (($nbytes=$proc_fh->read($buff,16384)) > 0) { #copy body from DSPAM
- $dspam_fh->print($buff) or die "Can't write to $dspam_fname: $!";
- }
- defined $nbytes or die "Error reading: $!";
- my($err); $proc_fh->close or $err = $!; my($retval) = $?;
- $dspam_fh->close or die "Error closing $dspam_fname: $!";
- $retval==0 && $err==0 && defined $first_line
- or do_log(-1,sprintf("WARN: DSPAM problem, %s, result=%s",
- exit_status_str($retval,$err), $first_line) );
- do_log(4,"spam_scan: DSPAM gave: $dspam_signature, $dspam_result");
- section_time('DSPAM');
- }
- # read mail into memory (horror!) in preparation for SpamAssasin
- $fh->seek(0,0) or die "Can't rewind mail file: $!";
- my($body_lines)=0; my($ln);
- for (undef $!; defined($ln=<$fh>); undef $!) # header
- { push(@lines,$ln); last if $ln eq $eol }
- defined $ln || $!==0 or die "Error reading mail header: $!";
- for (undef $!; defined($ln=<$fh>); undef $!) # body
- { push(@lines,$ln); $body_lines++ }
- defined $ln || $!==0 or die "Error reading mail body: $!";
- section_time('SA msg read');
- my($sa_required, $sa_tests);
- my($saved_umask) = umask; my($saved_pid) = $$;
- my($remaining_time) = alarm(0); # check how much time is left
- eval {
- # NOTE ON TIMEOUTS: SpamAssassin may use timer for its own purpose,
- # disabling it before returning. It seems it only uses timer when
- # external tests are enabled, so in order for our timeout to be
- # useful, $sa_local_tests_only needs to be true (e.g. 1).
- local $SIG{ALRM} = sub {
- my($s) = Carp::longmess("SA TIMED OUT, backtrace:");
- # crop at some rather arbitrary limit
- if (length($s) > 900) { $s = substr($s,0,900-3) . "..." }
- do_log(-1,$s);
- };
- # prepared to wait no more than n seconds
- alarm($sa_timeout) if $sa_timeout > 0;
- my($mail_obj); my($sa_version) = Mail::SpamAssassin::Version();
- do_log(5,"calling SA parse, SA version $sa_version");
- #first save our spamassassin config
- my %conf_backup = ();
- $spamassassin_obj->copy_config(undef, \%conf_backup) ||
- die "config: error returned from copy_config!\n";
- do_log(4,"SA Config saved");
- # *** note that $sa_version could be 3.0.1, which is not really numeric!
- if ($sa_version=~/^(\d+(?:\.\d+)?)/ && $1 >= 3) {
- my($pbname) = c('policy_bank_name');
- if ($pbname ne '') {
- my ($rule_name) = c('sa_site_rules_filename');
- $pbname =~ s/^pb_//;
- if ($rule_name) {
- $spamassassin_obj->read_scoreonly_config ($rule_name);
- } else {
- $spamassassin_obj->read_scoreonly_config ('/etc/spamassassin/multiconf/10_' . $pbname . ".cf");
- }
- }
- $mail_obj = $spamassassin_obj->parse(\@lines);
- } else { # 2.63 or earlier
- $mail_obj = Mail::SpamAssassin::NoMailAudit->new(data => \@lines,
- add_From_line => 0);
- }
- section_time('SA parse');
- do_log(4,"CALLING SA check");
- my($per_msg_status);
- { local($1,$2,$3,$4,$5,$6); # avoid Perl 5.8.0 bug, $1 gets tainted
- $per_msg_status = $spamassassin_obj->check($mail_obj);
- }
- my($rem_t) = alarm(0);
- do_log(4,"RETURNED FROM SA check, time left: $rem_t s");
- { local($1,$2,$3,$4); # avoid Perl 5.8.0..5.8.3...? taint bug
- $spam_level = $per_msg_status->get_hits;
- $sa_required = $per_msg_status->get_required_hits; # not used
- if ($sa_version=~/^(\d+(?:\.\d+)?)/ && $1 >= 3) {
- # access private SA method, unsupported
- $sa_tests = $per_msg_status->_get_tag('TESTSSCORES',',');
- $autolearn_status = $per_msg_status->get_autolearn_status;
- } else {
- $sa_tests = $per_msg_status->get_names_of_tests_hit;
- }
- $spam_report = $per_msg_status->get_report; # taints $1 and $2 !
- # example of how to gather aditional information from SA:
- # my($trusted) = $per_msg_status->_get_tag('RELAYSTRUSTED');
- # $hdr_edits->append_header('X-TESTING',$trusted);
- #Experimental, unfinished:
- # $per_msg_status->rewrite_mail;
- # my($entity) = nomailaudit_to_mime_entity($mail_obj);
- $per_msg_status->finish;
- #now copy our config back
- $spamassassin_obj->copy_config(\%conf_backup, undef) ||
- die "config: error returned from copy_config!\n";
- do_log(4,"SA Config restored");
- }
- };
- section_time('SA check');
- umask($saved_umask); # SA changes umask to 0077
- if ($$ != $saved_pid) {
- eval { do_log(-2,"PANIC, SA produced a clone process ".
- "of [$saved_pid], TERMINATING CLONE [$$]") };
- POSIX::_exit(1); # avoid END and destructor processing
- }
- prolong_timer('spam_scan_SA', $remaining_time); # restart the timer
- if ($@ ne '') { # SA timed out?
- chomp($@);
- die "$@\n" if $@ ne "timed out";
- }
- $sa_tests =~ s/,\s*/,/g; $spam_status = "tests=[" . $sa_tests . "]";
- add_entropy($spam_level,$sa_tests);
- if (defined $dspam && $dspam ne '' && defined $spam_level) { # auto-learn
- my($eat,@options);
- @options = (qw(--stdout --mode=tum --user), $daemon_user); # --mode=teft
- if ( $spam_level > 7.0 && $dspam_result eq 'Innocent') {
- $eat = 'SPAM'; push(@options, qw(--class=spam --source=error));
- }
- elsif ($spam_level < 0.5 && $dspam_result eq 'Spam') {
- $eat = 'HAM'; push(@options, qw(--class=innocent --source=error));
- }
- if (defined $eat && $dspam_signature ne '') {
- do_log(2,"DSPAM learn $eat ($spam_level), $dspam_signature");
- my($proc_fh,$pid) = run_command($dspam_fname, "&1", $dspam, @options);
- # consume remaining output to avoid broken pipe
- my($nbytes,$buff);
- while (($nbytes=$proc_fh->read($buff,4096)) > 0) { }
- defined $nbytes or die "Error reading from DSPAM process: $!";
- my($err); $proc_fh->close or $err = $!; my($retval) = $?;
- # do_log(-1,"DSPAM learn $eat response:".$output) if $output ne '';
- $retval==0 && $err==0
- or die ("DSPAM learn $eat FAILED: ".exit_status_str($retval,$err));
- section_time('DSPAM learn');
- }
- }
- }
- if (defined $dspam_fname) {
- if (($spam_level > 5.0 ? 1 : 0) != ($dspam_result eq 'Spam' ? 1 : 0))
- { do_log(2,"DSPAM: different opinions: $dspam_result, $spam_level") }
- unlink($dspam_fname) or die "Can't delete file $dspam_fname: $!";
- }
- do_log(3,"spam_scan: score=$spam_level $spam_status");
- ($spam_level, $spam_status, $spam_report, $autolearn_status);
- }
- #sub nomailaudit_to_mime_entity($) {
- # my($mail_obj) = @_; # expect a Mail::SpamAssassin::MsgContainer object
- # my(@m_hdr) = $mail_obj->header; # in array context returns array of lines
- # my($m_body) = $mail_obj->body; # returns array ref
- # my($entity);
- # # make sure _our_ source line number is reported in case of failure
- # eval {$entity = MIME::Entity->build(
- # Type => 'text/plain', Encoding => '-SUGGEST',
- # Data => $m_body); 1} or do {chomp($@); die $@};
- # my($head) = $entity->head;
- # # insert header fields from template into MIME::Head entity
- # for my $hdr_line (@m_hdr) {
- # # make sure _our_ source line number is reported in case of failure
- # eval {$head->replace($fhead,$fbody); 1} or do {chomp($@); die $@};
- # }
- # $entity; # return the built MIME::Entity
- #}
- 1;
- __DATA__
- #
- package Amavis::Unpackers;
- use strict;
- use re 'taint';
- BEGIN {
- use Exporter ();
- use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.043';
- @ISA = qw(Exporter);
- @EXPORT_OK = qw(&init &decompose_part &determine_file_types);
- }
- use Errno qw(ENOENT EACCES EAGAIN);
- use IO::File qw(O_CREAT O_EXCL O_WRONLY);
- use File::Basename qw(basename);
- use Convert::TNEF;
- use Convert::UUlib 1.05 qw(:constants); # avoid security bug in 1.04 and older
- use Compress::Zlib 1.35; # avoid security vulnerability in <= 1.34
- use Archive::Tar;
- use Archive::Zip 1.14 qw(:CONSTANTS :ERROR_CODES);
- BEGIN {
- import Amavis::Util qw(untaint min max ll do_log retcode exit_status_str
- snmp_count prolong_timer sanitize_str run_command
- rmdir_recursively add_entropy);
- import Amavis::Conf qw(:platform :confvars $file c cr ca);
- import Amavis::Timing qw(section_time);
- import Amavis::Lookup qw(lookup);
- import Amavis::Unpackers::MIME qw(mime_decode);
- import Amavis::Unpackers::NewFilename qw(consumed_bytes);
- }
- use subs @EXPORT_OK;
- # recursively descend into a directory $dir containing potentially unsafe
- # files with unpredictable names, soft links, etc., rename each regular
- # nonempty file to directory $outdir giving it a generated name,
- # and discard all the rest, including the directory $dir.
- # Return a pair: number of bytes that 'sanitized' files now occupy,
- # and a number of parts objects created.
- #
- sub flatten_and_tidy_dir($$$;$$); # prototype
- sub flatten_and_tidy_dir($$$;$$) {
- my($dir, $outdir, $parent_obj, $item_num_offset, $orig_names) = @_;
- do_log(4, "flatten_and_tidy_dir: processing directory \"$dir\"");
- my($cnt_r,$cnt_u) = (0,0); my($consumed_bytes) = 0;
- my($item_num) = 0; my($parent_placement) = $parent_obj->mime_placement;
- chmod(0750, $dir) or die "Can't change protection of \"$dir\": $!";
- local(*DIR); opendir(DIR,$dir) or die "Can't open directory \"$dir\": $!";
- my(@dirfiles) = readdir(DIR); # must avoid modifying dir. while traversing it
- closedir(DIR) or die "Error closing directory \"$dir\": $!";
- for my $f (@dirfiles) {
- my($msg); my($fname) = "$dir/$f";
- my(@stat_list) = lstat($fname); my($errn) = @stat_list ? 0 : 0+$!;
- if ($errn == ENOENT) { $msg = "does not exist" }
- elsif ($errn) { $msg = "inaccessible: $!" }
- if (defined $msg) { die "flatten_and_tidy_dir: \"$fname\" $msg," }
- next if ($f eq '.' || $f eq '..') && -d _;
- add_entropy(@stat_list);
- my($newpart_obj) = Amavis::Unpackers::Part->new($outdir,$parent_obj);
- $item_num++;
- $newpart_obj->mime_placement(sprintf("%s/%d",$parent_placement,
- $item_num+$item_num_offset) );
- # save tainted original member name if available, or a tainted file name
- my($original_name) = !ref($orig_names) ? undef : $orig_names->{$f};
- $newpart_obj->name_declared(defined $original_name ? $original_name : $f);
- # untaint, but if $dir happens to still be tainted, we want to know and die
- $fname = $dir.'/'.untaint($f);
- if (-d _) {
- $newpart_obj->attributes_add('D');
- my($bytes,$cnt) = flatten_and_tidy_dir($fname, $outdir, $parent_obj,
- $item_num+$item_num_offset, $orig_names);
- $consumed_bytes += $bytes; $item_num += $cnt;
- } elsif (-l _) {
- $cnt_u++; $newpart_obj->attributes_add('L');
- unlink($fname) or die "Can't remove soft link \"$fname\": $!";
- } elsif (!-f _) {
- do_log(4, "flatten_and_tidy_dir: NONREGULAR FILE \"$fname\"");
- $cnt_u++; $newpart_obj->attributes_add('S');
- unlink($fname) or die "Can't remove nonregular file \"$fname\": $!";
- } elsif (-z _) {
- $cnt_u++;
- unlink($fname) or die "Can't remove empty file \"$fname\": $!";
- } else {
- chmod(0750, $fname)
- or die "Can't change protection of file \"$fname\": $!";
- my($size) = 0 + (-s _);
- $newpart_obj->size($size);
- $consumed_bytes += $size;
- my($newpart) = $newpart_obj->full_name;
- ll(5) && do_log(5,
- sprintf("flatten_and_tidy_dir: renaming \"%s\"%s to %s", $fname,
- !defined $original_name ? '' : " ($original_name)", $newpart));
- $cnt_r++;
- rename($fname, $newpart)
- or die "Can't rename \"$fname\" to $newpart: $!";
- }
- }
- rmdir($dir) or die "Can't remove directory \"$dir\": $!";
- section_time("ren$cnt_r-unl$cnt_u-files$item_num");
- ($consumed_bytes, $item_num);
- }
- # call 'file(1)' utility for each part,
- # and associate (save) full and short types with each part
- #
- sub determine_file_types($$) {
- my($tempdir, $partslist_ref) = @_;
- $file ne '' or die "Unix utility file(1) not available, but is needed";
- my($cwd) = "$tempdir/parts";
- my(@part_list) = grep { $_->exists } @$partslist_ref;
- if (!@part_list) { do_log(5, "no parts, file(1) not called") }
- else {
- local($1,$2); # avoid Perl taint bug (5.8.3), $cwd and $arg are not tainted
- # but $arg becomes tainted because $1 is tainted from before
- my(@file_list) = # collect full file names, remove cwd if possible
- map { my($n) = $_->full_name; $n =~ s{^\Q$cwd\E/(.*)\z}{$1}s; $n }
- @part_list;
- chdir($cwd) or die "Can't chdir to $cwd: $!";
- my($proc_fh,$pid) = run_command(undef, "&1", $file, @file_list);
- chdir($TEMPBASE) or die "Can't chdir to $TEMPBASE: $!";
- my($index)=0; my($ln);
- for (undef $!; defined($ln=$proc_fh->getline); undef $!) {
- chomp($ln);
- do_log(5, "result line from file(1): $ln");
- if ($index > $#file_list) {
- do_log(-1, "NOTICE: Skipping extra output from file(1): $ln");
- } else {
- my($part) = $part_list[$index]; # walk through @part_list in sync
- my($expect) = $file_list[$index]; # walk through @file_list in sync
- if ($ln !~ /^(\Q$expect\E):[ \t]*(.*)\z/s) { #split file name from type
- do_log(-1,"NOTICE: Skipping bad output from file(1) ".
- "at [$index, $expect], got: $ln");
- } else {
- my($type_short); my($actual_name) = $1; my($type_long) = $2;
- $type_short = lookup(0,$type_long,@map_full_type_to_short_type_maps);
- ll(4) && do_log(4, sprintf("File-type of %s: %s%s",
- $part->base_name, $type_long,
- (!defined $type_short ? ''
- : !ref $type_short ? "; ($type_short)"
- : '; (' . join(', ',@$type_short) . ')'
- ) ));
- $part->type_long($type_long); $part->type_short($type_short);
- $part->attributes_add('C') # simpleminded
- if !ref($type_short) ? $type_short eq 'pgp' # encrypted?
- : grep {$_ eq 'pgp'} @$type_short;
- $index++;
- }
- }
- }
- defined $ln || $!==0 || $!==EAGAIN
- or die "Error reading from file(1) utility: $!";
- if ($index < @part_list) {
- die sprintf("parsing file(1) results - missing last %d results",
- @part_list - $index);
- }
- my($err); $proc_fh->close or $err = $!;
- $?==0 or die ("'file' utility ($file) failed, ".exit_status_str($?,$err));
- section_time(sprintf('get-file-type%d', scalar(@part_list)));
- }
- }
- sub decompose_mail($$) {
- my($tempdir,$file_generator_object) = @_;
- my($hold); my(@parts); my($depth) = 1; my($any_undecipherable) = 0;
- my($which_section) = "parts_decode";
- # fetch all not-yet-visited part names, and start a new cycle
- TIER:
- while (@parts = @{$file_generator_object->parts_list}) {
- if ($MAXLEVELS && $depth > $MAXLEVELS) {
- $hold = "Maximum decoding depth ($MAXLEVELS) exceeded";
- last;
- }
- $file_generator_object->parts_list_reset; # new names cycle
- # clip to avoid very long log entries
- my(@chopped_parts) = @parts > 5 ? @parts[0..4] : @parts;
- ll(4) && do_log(4,sprintf("decode_parts: level=%d, #parts=%d : %s",
- $depth, scalar(@parts),
- join(', ', (map { $_->base_name } @chopped_parts),
- (@chopped_parts >= @parts ? () : "...")) ));
- for my $part (@parts) { # test for existence of all expected files
- my($fname) = $part->full_name;
- my($errn) = $fname eq '' ? ENOENT : lstat($fname) ? 0 : 0+$!;
- if ($errn == ENOENT) {
- $part->exists(0);
- # $part->type_short('no-file') if !defined $part->type_short;
- } elsif ($errn) {
- die "decompose_mail: inaccessible file $fname: $!";
- } elsif (!-f _) { # not a regular file
- my($what) = -l _ ? 'symlink' : -d _ ? 'directory' : 'non-regular file';
- do_log(-1, "WARN: decompose_mail: removing unexpected $what $fname");
- if (-d _) { rmdir_recursively($fname) }
- else { unlink($fname) or die "Can't delete $what $fname: $!" }
- $part->exists(0);
- $part->type_short(-l _ ? 'symlink' : -d _ ? 'dir' : 'special')
- if !defined $part->type_short;
- } elsif (-z _) { # empty file
- unlink($fname) or die "Can't remove \"$fname\": $!";
- $part->exists(0);
- $part->type_short('empty') if !defined $part->type_short;
- $part->type_long('empty') if !defined $part->type_long;
- } else {
- $part->exists(1);
- }
- }
- determine_file_types($tempdir, \@parts);
- for my $part (@parts) {
- if ($part->exists && !defined($hold))
- { $hold = decompose_part($part, $tempdir) }
- $any_undecipherable++ if grep {$_ eq 'U'} @{ $part->attributes || [] };
- }
- last TIER if defined $hold;
- $depth++;
- }
- section_time($which_section); prolong_timer($which_section);
- ($hold, $any_undecipherable);
- }
- # Decompose the part
- sub decompose_part($$) {
- my($part, $tempdir) = @_;
- # possible return values from eval:
- # 0 - truly atomic, or unknown or archiver failure; consider atomic
- # 1 - some archive, successfully unpacked, result replaces original
- # 2 - probably unpacked, but keep the original (eg self-extracting archive)
- my($hold,$none_called);
- my($sts) = eval {
- my($type_short) = $part->type_short;
- my(@ts) = !defined $type_short ? ()
- : !ref $type_short ? ($type_short) : @$type_short;
- return 0 if !@ts; # consider atomic if unknown (returns from eval)
- snmp_count("OpsDecType-".join('.',@ts));
- for my $dec_tuple (@{ca('decoders')}) { # first matching decoder wins
- next if !defined $dec_tuple;
- my($dec_ts,$code,@args) = @$dec_tuple;
- if ($code && grep {$_ eq $dec_ts} @ts)
- { return &$code($part,$tempdir,@args) } # returns from eval
- }
- # falling through (e.g. HTML) - no match, consider atomic
- $none_called = 1;
- return 0; # returns from eval
- };
- if ($@ ne '') {
- chomp($@);
- if ($@ =~ /^Exceeded storage quota/ ||
- $@ =~ /^Maximum number of files\b.*\bexceeded/) { $hold = $@ }
- else {
- do_log(-1,sprintf("Decoding of %s (%s) failed, leaving it unpacked: %s",
- $part->base_name, $part->type_long, $@));
- }
- $sts = 2;
- chdir($TEMPBASE) or die "Can't chdir to $TEMPBASE: $!"; # just in case
- }
- if ($sts == 1 && lookup(0,$part->type_long, @keep_decoded_original_maps)) {
- # don't trust this file type or unpacker,
- # keep both the original and the unpacked file
- ll(4) && do_log(4,sprintf("file type is %s, retain original %s",
- $part->type_long, $part->base_name));
- $sts = 2;
- }
- if ($sts == 1) {
- ll(5) && do_log(5, "decompose_part: deleting ".$part->full_name);
- unlink($part->full_name)
- or die sprintf("Can't unlink %s: %s", $part->full_name, $!);
- }
- ll(4) && do_log(4,sprintf("decompose_part: %s - %s", $part->base_name,
- ['atomic','archive, unpacked','source retained']->[$sts]));
- section_time('decompose_part') unless $none_called;
- $hold;
- }
- # a trivial wrapper around mime_decode() to adjust arguments and result
- sub do_mime_decode($$) {
- my($part, $tempdir) = @_;
- mime_decode($part,$tempdir,$part);
- 2; # probably unpacked, but keep the original mail
- };
- #
- # Uncompression/unarchiving routines
- # Possible return codes:
- # 0 - truly atomic, or unknown or archiver failure; consider atomic
- # 1 - some archiver format, successfully unpacked, result replaces original
- # 2 - probably unpacked, but keep the original (eg self-extracting archive)
- # if ASCII text, try multiple decoding methods as provided by UUlib
- # (uuencoded, xxencoded, BinHex, yEnc, Base64, Quoted-Printable)
- sub do_ascii($$) {
- my($part, $tempdir) = @_;
- ll(4) && do_log(4,"do_ascii: Decoding part ".$part->base_name);
- snmp_count('OpsDecByUUlibAttempt');
- # prevent uunconc.c/UUDecode() from trying to create temp file in '/'
- my($old_env_tmpdir) = $ENV{TMPDIR}; $ENV{TMPDIR} = "$tempdir/parts";
- my($any_errors,$any_decoded);
- eval { # must not go away without calling Convert::UUlib::CleanUp!
- my($sts,$count);
- $sts = Convert::UUlib::Initialize();
- $sts = 0 if !defined($sts); #avoid Use of uninit. value in numeric eq (==)
- $sts==RET_OK or die "Convert::UUlib::Initialize failed: ".
- Convert::UUlib::strerror($sts);
- my($uulib_version) = Convert::UUlib::GetOption(OPT_VERSION);
- !Convert::UUlib::SetOption(OPT_IGNMODE,1) or die "bad uulib OPT_IGNMODE";
- # !Convert::UUlib::SetOption(OPT_DESPERATE,1) or die "bad uulib OPT_DESPERATE";
- ($sts, $count) = Convert::UUlib::LoadFile($part->full_name);
- if ($sts != RET_OK) {
- my($errmsg) = Convert::UUlib::strerror($sts) . ": $!";
- $errmsg .= ", (???"
- . Convert::UUlib::strerror(Convert::UUlib::GetOption(OPT_ERRNO))."???)"
- if $sts == RET_IOERR;
- die "Convert::UUlib::LoadFile (uulib V$uulib_version) failed: $errmsg";
- }
- ll(4) && do_log(4,sprintf(
- "do_ascii: Decoding part %s (%d items), uulib V%s",
- $part->base_name, $count, $uulib_version));
- my($uu);
- my($item_num) = 0; my($parent_placement) = $part->mime_placement;
- for (my($j) = 0; $uu = Convert::UUlib::GetFileListItem($j); $j++) {
- $item_num++;
- ll(4) && do_log(4,sprintf(
- "do_ascii(%d): state=0x%02x, enc=%s%s, est.size=%s, name=%s",
- $j, $uu->state, Convert::UUlib::strencoding($uu->uudet),
- ($uu->mimetype ne '' ? ", mimetype=" . $uu->mimetype : ''),
- $uu->size, $uu->filename));
- if (!($uu->state & FILE_OK)) {
- $any_errors++;
- do_log(1,"do_ascii: Convert::UUlib info: $j not decodable, ".$uu->state);
- } else {
- my($newpart_obj)=Amavis::Unpackers::Part->new("$tempdir/parts",$part);
- $newpart_obj->mime_placement("$parent_placement/$item_num");
- $newpart_obj->name_declared($uu->filename);
- my($newpart) = $newpart_obj->full_name;
- $! = undef;
- $sts = $uu->decode($newpart); # decode to file $newpart
- my($err_decode) = "$!";
- chmod(0750, $newpart) or $! == ENOENT # chmod, don't panic if no file
- or die "Can't change protection of \"$newpart\": $!";
- my($statmsg);
- my($errn) = lstat($newpart) ? 0 : 0+$!;
- if ($errn == ENOENT) { $statmsg = "does not exist" }
- elsif ($errn) { $statmsg = "inaccessible: $!" }
- elsif ( -l _) { $statmsg = "is a symlink" }
- elsif ( -d _) { $statmsg = "is a directory" }
- elsif (!-f _) { $statmsg = "not a regular file" }
- if (defined $statmsg) { $statmsg = "; file status: $newpart $statmsg" }
- my($size) = 0 + (-s _);
- $newpart_obj->size($size);
- consumed_bytes($size, 'do_ascii');
- if ($sts == RET_OK && $errn==0) {
- $any_decoded++;
- do_log(4,"do_ascii: RET_OK" . $statmsg) if defined $statmsg;
- } elsif ($sts == RET_NODATA || $sts == RET_NOEND) {
- $any_errors++;
- do_log(-1,"do_ascii: Convert::UUlib error: "
- . Convert::UUlib::strerror($sts) . $statmsg);
- } else {
- $any_errors++;
- my($errmsg) = Convert::UUlib::strerror($sts) . ":: $err_decode";
- $errmsg .= ", " . Convert::UUlib::strerror(
- Convert::UUlib::GetOption(OPT_ERRNO) ) if $sts == RET_IOERR;
- die ("Convert::UUlib failed: " . $errmsg . $statmsg);
- }
- }
- }
- };
- my($eval_stat) = $@;
- Convert::UUlib::CleanUp();
- snmp_count('OpsDecByUUlib') if $any_decoded;
- if (defined $old_env_tmpdir) { $ENV{TMPDIR} = $old_env_tmpdir }
- else { delete $ENV{TMPDIR} }
- if ($eval_stat ne '') { chomp($eval_stat); die "do_ascii: $eval_stat\n" }
- ($any_decoded && !$any_errors) ? 1 : $any_errors ? 2 : 0;
- }
- # use Archive-Zip
- sub do_unzip($$) {
- my($part, $tempdir) = @_;
- ll(4) && do_log(4, "Unzipping " . $part->base_name);
- snmp_count('OpsDecByArZipAttempt');
- my($zip) = Archive::Zip->new;
- my(@err_nm) = qw(AZ_OK AZ_STREAM_END AZ_ERROR AZ_FORMAT_ERROR AZ_IO_ERROR);
- # need to set up a temporary minimal error handler
- Archive::Zip::setErrorHandler(sub { return 5 });
- my($sts) = $zip->read($part->full_name);
- Archive::Zip::setErrorHandler(sub { die @_ });
- if ($sts != AZ_OK) {
- do_log(4, "do_unzip: not a zip: $err_nm[$sts] ($sts)");
- return 0;
- }
- my($any_unsupp_compmeth,$any_zero_length);
- my($encryptedcount,$extractedcount) = (0,0);
- my($item_num) = 0; my($parent_placement) = $part->mime_placement;
- for my $mem ($zip->members()) {
- my($newpart_obj) = Amavis::Unpackers::Part->new("$tempdir/parts",$part);
- $item_num++; $newpart_obj->mime_placement("$parent_placement/$item_num");
- $newpart_obj->name_declared($mem->fileName);
- my($compmeth) = $mem->compressionMethod;
- if ($compmeth != COMPRESSION_DEFLATED && $compmeth != COMPRESSION_STORED) {
- $any_unsupp_compmeth = $compmeth;
- $newpart_obj->attributes_add('U');
- } elsif ($mem->isEncrypted) {
- $encryptedcount++;
- $newpart_obj->attributes_add('U','C');
- } elsif ($mem->isDirectory) {
- $newpart_obj->attributes_add('D');
- } else {
- # want to read uncompressed - set to COMPRESSION_STORED
- my($oldc) = $mem->desiredCompressionMethod(COMPRESSION_STORED);
- $sts = $mem->rewindData();
- $sts == AZ_OK or die sprintf("%s: error rew. member data: %s (%s)",
- $part->base_name, $err_nm[$sts], $sts);
- my($newpart) = $newpart_obj->full_name;
- my($outpart) = IO::File->new;
- $outpart->open($newpart, O_CREAT|O_EXCL|O_WRONLY, 0640)
- or die "Can't create file $newpart: $!";
- binmode($outpart) or die "Can't set file $newpart to binmode: $!";
- my($size) = 0;
- while ($sts == AZ_OK) {
- my($buf_ref);
- ($buf_ref, $sts) = $mem->readChunk();
- $sts == AZ_OK || $sts == AZ_STREAM_END
- or die sprintf("%s: error reading member: %s (%s)",
- $part->base_name, $err_nm[$sts], $sts);
- my($buf_len) = length($$buf_ref);
- if ($buf_len > 0) {
- $size += $buf_len;
- $outpart->print($$buf_ref) or die "Can't write to $newpart: $!";
- consumed_bytes($buf_len, 'do_unzip');
- }
- }
- $any_zero_length = 1 if $size == 0;
- $newpart_obj->size($size);
- $outpart->close or die "Error closing $newpart: $!";
- $mem->desiredCompressionMethod($oldc);
- $mem->endRead();
- $extractedcount++;
- }
- }
- snmp_count('OpsDecByArZip');
- my($retval) = 1;
- if ($any_unsupp_compmeth) {
- $retval = 2;
- do_log(-1, sprintf("do_unzip: %s, unsupported compr. method: %s",
- $part->base_name, $any_unsupp_compmeth));
- } elsif ($any_zero_length) { # possible zip vulnerability exploit
- $retval = 2;
- do_log(1, sprintf("do_unzip: %s, zero length members, archive retained",
- $part->base_name));
- } elsif ($encryptedcount) {
- $retval = 2;
- do_log(1, sprintf(
- "do_unzip: %s, %d members are encrypted, %s extracted, archive retained",
- $part->base_name, $encryptedcount,
- !$extractedcount ? 'none' : $extractedcount));
- }
- $retval;
- }
- # use external decompressor program from the gzip/bzip2/compress family
- # (there *is* a perl module for bzip2, but is not ready for prime time)
- sub do_uncompress($$$) {
- my($part, $tempdir, $decompressor) = @_;
- ll(4) && do_log(4,sprintf("do_uncompress %s by %s",
- $part->base_name,$decompressor));
- my($decompressor_name) = basename((split(' ',$decompressor))[0]);
- snmp_count("OpsDecBy\u${decompressor_name}");
- my($newpart_obj) = Amavis::Unpackers::Part->new("$tempdir/parts",$part);
- $newpart_obj->mime_placement($part->mime_placement."/1");
- my($newpart) = $newpart_obj->full_name;
- my($type_short, $name_declared) = ($part->type_short, $part->name_declared);
- my(@rn); # collect recommended file names
- push(@rn,$1)
- if $part->type_long =~ /^\S+\s+compressed data, was "(.+)"(\z|, from\b)/;
- for my $name_d (!ref $name_declared ? ($name_declared) : @$name_declared) {
- next if $name_d eq '';
- my($name) = $name_d;
- for (!ref $type_short ? ($type_short) : @$type_short) {
- /^F\z/ and $name=~s/\.F\z//;
- /^Z\z/ and $name=~s/\.Z\z// || $name=~s/\.tg?z\z/.tar/;
- /^gz\z/ and $name=~s/\.gz\z// || $name=~s/\.tgz\z/.tar/;
- /^bz\z/ and $name=~s/\.bz\z// || $name=~s/\.tbz\z/.tar/;
- /^bz2\z/ and $name=~s/\.bz2?\z// || $name=~s/\.tbz\z/.tar/;
- /^lzo\z/ and $name=~s/\.lzo\z//;
- /^rpm\z/ and $name=~s/\.rpm\z/.cpio/;
- }
- push(@rn,$name) if !grep { $_ eq $name } @rn;
- }
- $newpart_obj->name_declared(@rn==1 ? $rn[0] : \@rn) if @rn;
- my($proc_fh,$pid) =
- run_command($part->full_name, undef, split(' ',$decompressor));
- my($rv,$rerr) = run_command_copy($newpart,$proc_fh);
- if ($rv) {
- # unlink($newpart) or die "Can't unlink $newpart: $!";
- die sprintf('Error running decompressor %s on %s, %s',
- $decompressor, $part->base_name, exit_status_str($rv,$rerr));
- }
- 1;
- }
- # use Compress::Zlib to inflate
- sub do_gunzip($$) {
- my($part, $tempdir) = @_; my($retval) = 0;
- do_log(4, "Inflating gzip archive " . $part->base_name);
- snmp_count('OpsDecByZlib');
- my($gz) = Amavis::IO::Zlib->new;
- $gz->open($part->full_name,'rb')
- or die ("do_gunzip: Can't open gzip file ".$part->full_name.": $!");
- my($newpart_obj) = Amavis::Unpackers::Part->new("$tempdir/parts",$part);
- $newpart_obj->mime_placement($part->mime_placement."/1");
- my($newpart) = $newpart_obj->full_name;
- my($outpart) = IO::File->new;
- $outpart->open($newpart, O_CREAT|O_EXCL|O_WRONLY, 0640)
- or die "Can't create file $newpart: $!";
- binmode($outpart) or die "Can't set file $newpart to binmode: $!";
- my($nbytes,$buff); my($size) = 0;
- while (($nbytes=$gz->read($buff,16384)) > 0) {
- $outpart->print($buff) or die "Can't write to $newpart: $!";
- $size += $nbytes; consumed_bytes($nbytes, 'do_gunzip');
- }
- my($err) = defined $nbytes ? 0 : $!;
- $newpart_obj->size($size);
- $outpart->close or die "Error closing $newpart: $!";
- my(@rn); # collect recommended file name
- my($name_declared) = $part->name_declared;
- for my $name_d (!ref $name_declared ? ($name_declared) : @$name_declared) {
- next if $name_d eq '';
- my($name) = $name_d;
- $name=~s/\.(gz|Z)\z// || $name=~s/\.tgz\z/.tar/;
- push(@rn,$name) if !grep { $_ eq $name } @rn;
- }
- $newpart_obj->name_declared(@rn==1 ? $rn[0] : \@rn) if @rn;
- if (defined $nbytes && $nbytes==0) { $retval = 1 } # success
- else {
- do_log(-1, "do_gunzip: Error reading file ".$part->full_name.": $err");
- unlink($newpart) or die "Can't unlink $newpart: $!";
- $newpart_obj->size(undef); $retval = 0;
- }
- $gz->close or die "Error closing gzipped file: $!";
- $retval;
- }
- # untar any tar archives with Archive-Tar, extract each file individually
- sub do_tar($$) {
- my($part, $tempdir) = @_;
- snmp_count('OpsDecByArTar');
- # Work around bug in Archive-Tar
- my $tar = eval { Archive::Tar->new($part->full_name) };
- if (!defined($tar)) {
- chomp($@);
- do_log(4, sprintf("Faulty archive %s: %s", $part->full_name, $@));
- return 0;
- }
- do_log(4,"Untarring ".$part->base_name);
- my($item_num) = 0; my($parent_placement) = $part->mime_placement;
- my(@list) = $tar->list_files();
- for (@list) {
- next if /\/\z/; # ignore directories
- # this is bad (reads whole file into scalar)
- # need some error handling, too
- my $data = $tar->get_content($_);
- my($newpart_obj) = Amavis::Unpackers::Part->new("$tempdir/parts",$part);
- $item_num++; $newpart_obj->mime_placement("$parent_placement/$item_num");
- my($newpart) = $newpart_obj->full_name;
- my($outpart) = IO::File->new;
- $outpart->open($newpart, O_CREAT|O_EXCL|O_WRONLY, 0640)
- or die "Can't create file $newpart: $!";
- binmode($outpart) or die "Can't set file $newpart to binmode: $!";
- $outpart->print($data) or die "Can't write to $newpart: $!";
- $newpart_obj->size(length($data));
- consumed_bytes(length($data), 'do_tar');
- $outpart->close or die "Error closing $newpart: $!";
- }
- 1;
- }
- # use external program to expand RAR archives
- sub do_unrar($$$) {
- my($part, $tempdir, $archiver) = @_;
- ll(4) && do_log(4, "Attempting to expand RAR archive " . $part->base_name);
- my($decompressor_name) = basename((split(' ',$archiver))[0]);
- snmp_count("OpsDecBy\u${decompressor_name}Attempt");
- my(@common_rar_switches) = qw(-c- -p- -av- -idp);
- my($err, $retval, $rv1);
- # unrar exit codes: SUCCESS=0, WARNING=1, FATAL_ERROR=2, CRC_ERROR=3,
- # LOCK_ERROR=4, WRITE_ERROR=5, OPEN_ERROR=6, USER_ERROR=7, MEMORY_ERROR=8,
- # CREATE_ERROR=9, USER_BREAK=255
- # Check whether we can really unrar it
- $rv1 = system($archiver, 't', '-inul', @common_rar_switches, '--',
- $part->full_name);
- $err = $!; $retval = retcode($rv1);
- if ($retval == 7) { # USER_ERROR
- do_log(-1,"do_unrar: $archiver does not recognize all switches, "
- . "it is probably too old. Retrying without '-av- -idp'. "
- . "Upgrade: http://www.rarlab.com/");
- @common_rar_switches = qw(-c- -p-); # retry without new switches
- $rv1 = system($archiver, 't', '-inul', @common_rar_switches, '--',
- $part->full_name);
- $err = $!; $retval = retcode($rv1);
- }
- if (!grep { $_ == $retval } (0,1,3)) {
- # not one of: SUCCESS, WARNING, CRC_ERROR
- # NOTE: password protected files in the archive cause CRC_ERROR
- do_log(4,sprintf("unrar 't' %s, command: %s",
- exit_status_str($rv1,$err), $archiver));
- return 0;
- }
- # We have to jump hoops because there is no simple way to
- # just list all the files
- ll(4) && do_log(4, "Expanding RAR archive " . $part->base_name);
- my(@list); my($hypcount) = 0; my($encryptedcount) = 0;
- my($lcnt) = 0; my($member_name); my($bytes) = 0; my($last_line);
- my($item_num) = 0; my($parent_placement) = $part->mime_placement;
- my($proc_fh,$pid) =
- run_command(undef, "&1", $archiver, 'v', @common_rar_switches, '--',
- $part->full_name);
- local($_);
- for (undef $!; defined($_=$proc_fh->getline); undef $!) {
- $last_line = $_ if !/^\s*$/; # keep last nonempty line
- chomp;
- if (/^unexpected end of archive/) {
- last;
- } elsif (/^------/) {
- $hypcount++;
- last if $hypcount >= 2;
- } elsif ($hypcount < 1 && /^Encrypted file:/) {
- do_log(4,"do_unrar: ".$_);
- $part->attributes_add('U','C');
- } elsif ($hypcount == 1) {
- $lcnt++; local($1,$2,$3);
- if ($lcnt % 2 == 0) { # information line (every other line)
- if (!/^\s+(\d+)\s+(\d+)\s+(\d+%|-->|<--)/) {
- do_log(-1,"do_unrar: can't parse info line for \"$member_name\" $_");
- } elsif (defined $member_name) {
- do_log(5,"do_unrar: member: \"$member_name\", size: $1");
- if ($1 > 0) { $bytes += $1; push(@list, $member_name) }
- }
- $member_name = undef;
- } elsif (/^(.)(.*)\z/s) {
- $member_name = $2; # all but the first character (space or an asterisk)
- if ($1 eq '*') { # member is encrypted
- $encryptedcount++; $item_num++;
- # make a phantom entry - carrying only name and attributes
- my($newpart_obj) =
- Amavis::Unpackers::Part->new("$tempdir/parts",$part);
- $newpart_obj->mime_placement("$parent_placement/$item_num");
- $newpart_obj->name_declared($member_name);
- $newpart_obj->attributes_add('U','C');
- $member_name = undef; # makes no sense extracting encrypted files
- }
- }
- }
- }
- defined $_ || $!==0 || $!==EAGAIN or die "Error reading: $!";
- # consume all remaining output to avoid broken pipe
- my($ln);
- for (undef $!; defined($ln=$proc_fh->getline); undef $!)
- { $last_line = $ln if $ln !~ /^\s*$/ }
- defined $ln || $!==0 || $!==EAGAIN or die "Error reading: $!";
- $err = undef; $proc_fh->close or $err = $!; $retval = retcode($?);
- if ($retval == 3) { # CRC_ERROR
- do_log(4,"do_unrar: CRC_ERROR - undecipherable");
- $part->attributes_add('U');
- }
- my($fn) = $part->full_name; local($1,$2);
- if (!$bytes && $retval==0 && $last_line =~ /^\Q$fn\E is not RAR archive$/) {
- do_log(4,"do_unrar: ".$last_line);
- return 0;
- } elsif ($last_line !~ /^\s*(\d+)\s+(\d+)/s) {
- do_log(4,"do_unrar: unable to obtain orig total size: $last_line");
- } else {
- do_log(4,"do_unrar: summary size: $2, sum of sizes: $bytes")
- if abs($bytes - $2) > 100;
- $bytes = $2 if $2 > $bytes;
- }
- consumed_bytes($bytes, 'do_unrar-pre', 1); # pre-check on estimated size
- snmp_count("OpsDecBy\u${decompressor_name}");
- if ($retval==0) {} # SUCCESS
- elsif ($retval==1 && @list && $bytes > 0) {} # WARNING, probably still ok
- else { # WARNING and suspicious, or really bad
- die ("unrar: can't get a list of archive members: " .
- exit_status_str($?,$err) ."; ".$last_line);
- }
- if (!@list) {
- do_log(4,"do_unrar: no archive members, or not an archive at all");
- #***return 0 if $exec;
- } else {
- # my $rv = store_mgr($tempdir, $part, \@list, $archiver,
- # qw(p -inul -kb), @common_rar_switches, '--',
- # $part->full_name);
- # unrar/rar can make the dir by itself, but can't hurt (sparc64 problem?)
- mkdir("$tempdir/parts/rar", 0750)
- or die "Can't mkdir $tempdir/parts/rar: $!";
- my($proc_fh,$pid) =
- run_command(undef, "&1", $archiver, qw(x -inul -ver -o- -kb),
- @common_rar_switches, '--',
- $part->full_name, "$tempdir/parts/rar/");
- my($nbytes,$buff); my($output) = '';
- while (($nbytes=$proc_fh->read($buff,4096)) > 0) { $output .= $buff }
- defined $nbytes or die "Error reading: $!";
- my($err); $proc_fh->close or $err = $!; my($retval) = retcode($?);
- if (!grep { $_ == $retval } (0,1,3)) { # not one of: SUCCESS, WARNING, CRC
- do_log(-1, 'unrar '.exit_status_str($?,$err));
- }
- my($errn) = lstat("$tempdir/parts/rar") ? 0 : 0+$!;
- if ($errn != ENOENT) {
- my($b) = flatten_and_tidy_dir("$tempdir/parts/rar","$tempdir/parts",$part);
- consumed_bytes($b, 'do_unrar');
- }
- }
- if ($encryptedcount) {
- do_log(1, sprintf(
- "do_unrar: %s, %d members are encrypted, %s extracted, archive retained",
- $part->base_name, $encryptedcount, !@list ? 'none' : 0+@list ));
- return 2;
- }
- 1;
- }
- # use external program to expand LHA archives
- sub do_lha($$$) {
- my($part, $tempdir, $archiver) = @_;
- ll(4) && do_log(4, "Attempting to expand LHA archive " . $part->base_name);
- my($decompressor_name) = basename((split(' ',$archiver))[0]);
- snmp_count("OpsDecBy\u${decompressor_name}Attempt");
- # lha needs extension .exe to understand SFX!
- symlink($part->full_name, $part->full_name.".exe")
- or die sprintf("Can't symlink %s %s.exe: %s",
- $part->full_name, $part->full_name, $!);
- # Check whether we can really lha it
- my($checkerr); my($retval) = 1; my($ln);
- my($proc_fh,$pid) =
- run_command(undef, "&1", $archiver, 'lq', $part->full_name.".exe");
- for (undef $!; defined($ln=$proc_fh->getline); undef $!)
- { $checkerr = 1 if /Checksum error/i }
- defined $ln || $!==0 || $!==EAGAIN or die "Error reading: $!";
- my($err); $proc_fh->close or $err = $!;
- if ($? || $checkerr) {
- $retval = 0; # consider atomic
- do_log(4, "do_lha: not a LHA archive($checkerr) ? ".
- exit_status_str($?,$err));
- } else {
- do_log(4, "Expanding LHA archive " . $part->base_name . ".exe");
- ($proc_fh,$pid) =
- run_command(undef, undef, $archiver, 'lq', $part->full_name.".exe");
- my(@list); my($ln);
- for (undef $!; defined($ln=$proc_fh->getline); undef $!) {
- chomp($ln); local($1);
- next if $ln =~ m{/\z}; # ignore directories
- if ($ln =~ /^(?:\S+\s+){6}\S+\s*(\S.*?)\s*\z/s) { push(@list,$1) }
- else { do_log(5,"do_lha: skip: $ln") }
- }
- defined $ln || $!==0 || $!==EAGAIN or die "Error reading: $!";
- $err=undef; $proc_fh->close or $err = $!;
- $?==0 or do_log(-1, 'do_lha: '.exit_status_str($?,$err));
- if (!@list) {
- do_log(4, "do_lha: no archive members, or not an archive at all");
- #*** $retval = 0 if $exec;
- } else {
- snmp_count("OpsDecBy\u${decompressor_name}");
- my $rv = store_mgr($tempdir, $part, \@list, $archiver, 'pq',
- $part->full_name.".exe");
- do_log(-1, 'do_lha '.exit_status_str($rv)) if $rv;
- $retval = 1; # consider decoded
- }
- }
- unlink($part->full_name.".exe")
- or die "Can't unlink " . $part->full_name . ".exe: $!";
- $retval;
- }
- # use external program to expand ARC archives;
- # works with original arc, or a GPL licensed 'nomarch'
- # (http://rus.members.beeb.net/nomarch.html)
- sub do_arc($$$) {
- my($part, $tempdir, $archiver) = @_;
- my($decompressor_name) = basename((split(' ',$archiver))[0]);
- snmp_count("OpsDecBy\u${decompressor_name}");
- my($is_nomarch) = $archiver =~ /nomarch/i;
- ll(4) && do_log(4,sprintf("Unarcing %s, using %s",
- $part->base_name, ($is_nomarch ? "nomarch" : "arc") ));
- my($cmdargs) = ($is_nomarch ? "-l -U" : "ln") . " " . $part->full_name;
- my($proc_fh,$pid) =
- run_command(undef, '/dev/null', $archiver, split(' ',$cmdargs));
- my(@list); my($ln);
- for (undef $!; defined($ln=$proc_fh->getline); undef $!) { push(@list,$ln) }
- defined $ln || $!==0 || $!==EAGAIN or die "Error reading: $!";
- my($err) = 0; $proc_fh->close or $err = $!;
- $err==0 && $?==0 or do_log(-1, 'do_arc: '.exit_status_str($?,$err));
- #*** no spaces in filenames allowed???
- map { s/^([^ \t\r\n]*).*\z/$1/s } @list; # keep only filenames
- if (@list) {
- my $rv = store_mgr($tempdir, $part, \@list, $archiver,
- ($is_nomarch ? ('-p', '-U') : 'p'), $part->full_name);
- do_log(-1, 'arc '.exit_status_str($rv)) if $rv;
- }
- 1;
- }
- # use external program to expand ZOO archives
- sub do_zoo($$$) {
- my($part, $tempdir, $archiver) = @_;
- do_log(4, "Expanding ZOO archive " . $part->full_name);
- my($decompressor_name) = basename((split(' ',$archiver))[0]);
- snmp_count("OpsDecBy\u${decompressor_name}");
- # Zoo needs extension of .zoo!
- symlink($part->full_name, $part->full_name.".zoo")
- or die sprintf("Can't symlink %s %s.zoo: %s",
- $part->full_name, $part->full_name, $!);
- my($proc_fh,$pid) =
- run_command(undef, undef, $archiver, 'lf1q', $part->full_name.".zoo");
- my(@list); my($ln);
- for (undef $!; defined($ln=$proc_fh->getline); undef $!) { push(@list,$ln) }
- defined $ln || $!==0 || $!==EAGAIN or die "Error reading: $!";
- my($err); $proc_fh->close or $err = $!;
- $?==0 or do_log(-1, 'do_zoo: '.exit_status_str($?,$err));
- if (@list) {
- chomp(@list);
- my $rv = store_mgr($tempdir, $part, \@list, $archiver, 'xpqqq:',
- $part->full_name . ".zoo");
- do_log(-1, 'zoo '.exit_status_str($rv)) if $rv;
- }
- unlink($part->full_name.".zoo")
- or die "Can't unlink " . $part->full_name . ".zoo: $!";
- 1;
- }
- # use external program to expand ARJ archives
- sub do_unarj($$$) {
- my($part, $tempdir, $archiver) = @_;
- do_log(4, "Expanding ARJ archive " . $part->base_name);
- my($decompressor_name) = basename((split(' ',$archiver))[0]);
- snmp_count("OpsDecBy\u${decompressor_name}");
- # options to arj, ignored by unarj
- # provide some password in -g to turn fatal error into 'bad password' error
- $ENV{ARJ_SW} = "-i -jo -b5 -2h -jyc -ja1 -gsecret -w$tempdir/parts";
- # unarj needs extension of .arj!
- symlink($part->full_name, $part->full_name.".arj")
- or die sprintf("Can't symlink %s %s.arj: %s",
- $part->full_name, $part->full_name, $!);
- # obtain total original size of archive members from the index/listing
- my($proc_fh,$pid) =
- run_command(undef,'/dev/null', $archiver, 'l', $part->full_name.".arj");
- my($last_line); my($ln);
- for (undef $!; defined($ln=$proc_fh->getline); undef $!)
- { $last_line = $ln if $ln !~ /^\s*$/ }
- defined $ln || $!==0 || $!==EAGAIN or die "Error reading: $!";
- my($err); $proc_fh->close or $err = $!; my($retval) = retcode($?);
- if (!grep { $_ == $retval } (0,1,3)) { # not one of: success, warn, CRC err
- die ("unarj: can't get a list of archive members: ".
- exit_status_str($?,$err));
- }
- if ($last_line !~ /^\s*(\d+)\s*files\s*(\d+)/s) {
- do_log(-1,"do_unarj: WARN: unable to obtain orig size of files: $last_line");
- } else {
- consumed_bytes($2, 'do_unarj-pre', 1); # pre-check on estimated size
- }
- # unarj has very limited extraction options, arj is much better!
- mkdir("$tempdir/parts/arj", 0750) or die "Can't mkdir $tempdir/parts/arj: $!";
- chdir("$tempdir/parts/arj") or die "Can't chdir to $tempdir/parts/arj: $!";
- ($proc_fh,$pid) =
- run_command(undef, "&1", $archiver, 'e', $part->full_name.".arj");
- my($encryptedcount,$skippedcount) = (0,0);
- for (undef $!; defined($ln=$proc_fh->getline); undef $!) {
- $encryptedcount++
- if $ln =~ /^(Extracting.*\bBad file data or bad password|File is password encrypted, Skipped)\b/s;
- $skippedcount++
- if $ln =~ /(\bexists|^File is password encrypted|^Unsupported .*), Skipped\b/s;
- }
- defined $ln || $!==0 || $!==EAGAIN or die "Error reading: $!";
- $err = undef; $proc_fh->close or $err = $!; $retval = retcode($?);
- chdir($TEMPBASE) or die "Can't chdir to $TEMPBASE: $!";
- if (!grep { $_ == $retval } (0,1,3)) { # not one of: success, warn, CRC err
- do_log(0, "unarj: error extracting: ".exit_status_str($?,$err));
- }
- # add attributes to the parent object, because we didn't remember names
- # of its scrambled members
- $part->attributes_add('U') if $skippedcount;
- $part->attributes_add('C') if $encryptedcount;
- my($errn) = lstat("$tempdir/parts/arj") ? 0 : 0+$!;
- if ($errn != ENOENT) {
- my($b) = flatten_and_tidy_dir("$tempdir/parts/arj","$tempdir/parts",$part);
- consumed_bytes($b, 'do_unarj');
- snmp_count("OpsDecBy\u${decompressor_name}");
- }
- unlink($part->full_name.".arj")
- or die "Can't unlink " . $part->full_name . ".arj: $!";
- if (!grep { $_ == $retval } (0,1,3)) { # not one of: success, warn, CRC err
- die ("unarj: can't extract archive members: ".exit_status_str($?,$err));
- }
- if ($encryptedcount || $skippedcount) {
- do_log(1, sprintf(
- "do_unarj: %s, %d members are encrypted, %d skipped, archive retained",
- $part->base_name, $encryptedcount, $skippedcount));
- return 2;
- }
- 1;
- }
- # use external program to expand TNEF archives
- sub do_tnef_ext($$$) {
- my($part, $tempdir, $archiver) = @_;
- do_log(4, "Extracting from TNEF encapsulation (ext) " . $part->base_name);
- my($archiver_name) = basename((split(' ',$archiver))[0]);
- snmp_count("OpsDecBy\u${archiver_name}");
- mkdir("$tempdir/parts/tnef",0750)
- or die "Can't mkdir $tempdir/parts/tnef: $!";
- my($proc_fh,$pid) = run_command(undef, "&1", $archiver, '--number-backups',
- '-C', "$tempdir/parts/tnef", '-f', $part->full_name);
- my($nbytes,$buff); my($output) = '';
- while (($nbytes=$proc_fh->read($buff,4096)) > 0) { $output .= $buff }
- defined $nbytes or die "Error reading: $!";
- my($err); $proc_fh->close or $err = $!;
- $?==0 or do_log(0, 'tnef '.exit_status_str($?,$err).' '.$output);
- my($b) = flatten_and_tidy_dir("$tempdir/parts/tnef","$tempdir/parts",$part);
- if ($b > 0) {
- do_log(4, "tnef extracted $b bytes from a tnef container");
- consumed_bytes($b, 'do_tnef');
- }
- 1;
- }
- # use Convert-TNEF
- sub do_tnef($$) {
- my($part, $tempdir) = @_;
- do_log(4, "Extracting from TNEF encapsulation (int) " . $part->base_name);
- snmp_count('OpsDecByTnef');
- my($tnef) = Convert::TNEF->read_in($part->full_name,
- {output_dir=>"$tempdir/parts", buffer_size=>16384, ignore_checksum=>1});
- defined $tnef or die "Convert::TNEF failed: ".$Convert::TNEF::errstr;
- my($item_num) = 0; my($parent_placement) = $part->mime_placement;
- for my $a ($tnef->message, $tnef->attachments) {
- for my $attr_name ('AttachData','Attachment') {
- my($dh) = $a->datahandle($attr_name);
- if (defined $dh) {
- my($newpart_obj)= Amavis::Unpackers::Part->new("$tempdir/parts",$part);
- $item_num++;
- $newpart_obj->mime_placement("$parent_placement/$item_num");
- $newpart_obj->name_declared([$a->name, $a->longname]);
- my($newpart) = $newpart_obj->full_name;
- my($outpart) = IO::File->new;
- $outpart->open($newpart, O_CREAT|O_EXCL|O_WRONLY, 0640)
- or die "Can't create file $newpart: $!";
- binmode($outpart) or die "Can't set file $newpart to binmode: $!";
- my($file) = $dh->path; my($size) = 0;
- if (defined $file) {
- my($io,$nbytes,$buff); $dh->binmode(1);
- $io = $dh->open("r") or die "Can't open MIME::Body handle: $!";
- while (($nbytes=$io->read($buff,16384)) > 0) {
- $outpart->print($buff) or die "Can't write to $newpart: $!";
- $size += $nbytes; consumed_bytes($nbytes, 'do_tnef_1');
- }
- defined $nbytes or die "Error reading from MIME::Body handle: $!";
- $io->close or die "Error closing MIME::Body handle: $!";
- } else {
- my($buff) = $dh->as_string; my($nbytes) = length($buff);
- $outpart->print($buff) or die "Can't write to $newpart: $!";
- $size += $nbytes; consumed_bytes($nbytes, 'do_tnef_2');
- }
- $newpart_obj->size($size);
- $outpart->close or die "Error closing $newpart: $!";
- }
- }
- }
- $tnef->purge if defined $tnef;
- 1;
- }
- # The pax and cpio utilities usually support the following archive formats:
- # cpio, bcpio, sv4cpio, sv4crc, tar (old tar), ustar (POSIX.2 tar).
- # The utilities from http://heirloom.sourceforge.net/ support
- # several other tar/cpio variants such as SCO, Sun, DEC, Cray, SGI
- sub do_pax_cpio($$$) {
- my($part, $tempdir, $archiver) = @_;
- my($archiver_name) = basename((split(' ',$archiver))[0]);
- snmp_count("OpsDecBy\u${archiver_name}");
- ll(4) && do_log(4,sprintf("Expanding archive %s, using %s",
- $part->base_name, $archiver_name));
- my($is_pax) = $archiver_name =~ /^cpio/i ? 0 : 1;
- do_log(-1,"WARN: Using $archiver_name instead of pax can be a security ".
- "risk; please add: \$pax='pax'; to amavisd.conf and check that ".
- "the pax(1) utility is available on the system!") if !$is_pax;
- my(@cmdargs) = $is_pax ? qw(-v) : qw(-i -t -v);
- my($proc_fh,$pid) = run_command($part->full_name, undef, $archiver,@cmdargs);
- my($bytes) = 0; local($1,$2,$3); local($_);
- for (undef $!; defined($_=$proc_fh->getline); undef $!) {
- chomp;
- next if /^\d+ blocks\z/;
- last if /^(cpio|pax): (.*bytes read|End of archive volume)/;
- if (!/^ (?: \S+\s+ ){4}
- (\d+) \s+
- ( (?: \s* \S+ ){3} (?: \s+ \d{4}, )? ) \s+
- (.+) \z/xs) {
- do_log(-1,"do_pax_cpio: can't parse toc line: $_");
- } else {
- my($mem,$size) = ($3,$1);
- $mem = $1 if $is_pax && $mem =~ /^(.*) =[=>] (.*)\z/; # hard or soft link
- do_log(5,"do_pax_cpio: member: \"$mem\", size: $size");
- $bytes += $size if $size > 0;
- }
- }
- defined $_ || $!==0 || $!==EAGAIN or die "Error reading: $!";
- # consume remaining output to avoid broken pipe
- my($nbytes,$buff);
- while (($nbytes=$proc_fh->read($buff,4096)) > 0) { }
- defined $nbytes or die "Error reading: $!";
- my($err); $proc_fh->close or $err = $!;
- $?==0 or do_log(-1, 'do_pax_cpio/1: '.exit_status_str($?,$err));
- consumed_bytes($bytes, 'do_pax_cpio/pre', 1); # pre-check on estimated size
- mkdir("$tempdir/parts/arch", 0750)
- or die "Can't mkdir $tempdir/parts/arch: $!";
- my($name_clash) = 0;
- my(%orig_names); # maps filenames to archive member names when possible
- eval {
- chdir("$tempdir/parts/arch")
- or die "Can't chdir to $tempdir/parts/arch: $!";
- my(@cmdargs) = $is_pax ? qw(-r -k -p am -s /[^A-Za-z0-9_]/-/gp)
- : qw(-i -d --no-absolute-filenames --no-preserve-owner);
- my($proc_fh,$pid) = run_command($part->full_name,"&1",$archiver,@cmdargs);
- my($output) = ''; my($ln);
- for (undef $!; defined($ln=$proc_fh->getline); undef $!) {
- chomp($ln);
- if (!$is_pax || $ln !~ /^(.*) >> (\S*)\z/) { $output .= $ln."\n" }
- else { # parse output from pax -s///p
- my($member_name,$file_name) = ($1,$2);
- if (!exists $orig_names{$file_name}) {
- $orig_names{$file_name} = $member_name;
- } else {
- do_log(0,sprintf("do_pax_cpio: member \"%s\" is hidden by a ".
- "previous archive member \"%s\", file: %s",
- $member_name, $orig_names{$file_name}, $file_name));
- $orig_names{$file_name} = undef; # cause it to exist but undefined
- $name_clash++;
- }
- }
- }
- defined $ln || $!==0 || $!==EAGAIN or die "Error reading: $!";
- chomp($output); my($err); $proc_fh->close or $err = $!;
- $?==0 or die (exit_status_str($?,$err).' '.$output);
- };
- my($eval_stat) = $@;
- chdir($TEMPBASE) or die "Can't chdir to $TEMPBASE: $!";
- my($b) = flatten_and_tidy_dir("$tempdir/parts/arch", "$tempdir/parts",
- $part, 0, \%orig_names);
- consumed_bytes($b, 'do_pax_cpio');
- if ($eval_stat ne '') { chomp($eval_stat); die "do_pax_cpio: $eval_stat\n" }
- $name_clash ? 2 : 1;
- }
- # ar is a standard Unix binary archiver, also used by Debian packages
- sub do_ar($$$) {
- my($part, $tempdir, $archiver) = @_;
- ll(4) && do_log(4,"Expanding Unix ar archive ".$part->full_name);
- my($archiver_name) = basename((split(' ',$archiver))[0]);
- snmp_count("OpsDecBy\u${archiver_name}");
- my($proc_fh,$pid) = run_command(undef,undef,$archiver,'tv',$part->full_name);
- my($ln); my($bytes) = 0; local($1,$2,$3);
- for (undef $!; defined($ln=$proc_fh->getline); undef $!) {
- chomp($ln);
- if ($ln !~ /^(?:\S+\s+){2}(\d+)\s+((?:\S+\s+){3}\S+)\s+(.*)\z/) {
- do_log(-1,"do_ar: can't parse contents listing line: $ln");
- } else {
- do_log(5,"do_ar: member: \"$3\", size: $1");
- $bytes += $1 if $1 > 0;
- }
- }
- defined $ln || $!==0 || $!==EAGAIN or die "Error reading: $!";
- # consume remaining output to avoid broken pipe
- my($nbytes,$buff);
- while (($nbytes=$proc_fh->read($buff,4096)) > 0) { }
- defined $nbytes or die "Error reading: $!";
- my($err); $proc_fh->close or $err = $!;
- $?==0 or do_log(-1, 'ar-1 '.exit_status_str($?,$err));
- consumed_bytes($bytes, 'do_ar-pre', 1); # pre-check on estimated size
- mkdir("$tempdir/parts/ar", 0750)
- or die "Can't mkdir $tempdir/parts/ar: $!";
- chdir("$tempdir/parts/ar") or die "Can't chdir to $tempdir/parts/ar: $!";
- ($proc_fh,$pid) = run_command(undef, "&1", $archiver, 'x', $part->full_name);
- my($output) = '';
- while (($nbytes=$proc_fh->read($buff,4096)) > 0) { $output .= $buff }
- defined $nbytes or die "Error reading: $!";
- $err = undef; $proc_fh->close or $err = $!;
- $?==0 or do_log(-1, 'ar-2 '.exit_status_str($?,$err).' '.$output);
- chdir($TEMPBASE) or die "Can't chdir to $TEMPBASE: $!";
- my($b) = flatten_and_tidy_dir("$tempdir/parts/ar","$tempdir/parts",$part);
- consumed_bytes($b, 'do_ar');
- 1;
- }
- sub do_cabextract($$$) {
- my($part, $tempdir, $archiver) = @_;
- do_log(4, "Expanding cab archive " . $part->base_name);
- my($archiver_name) = basename((split(' ',$archiver))[0]);
- snmp_count("OpsDecBy\u${archiver_name}");
- local($_); my($bytes) = 0; my($ln);
- my($proc_fh,$pid) =
- run_command(undef,undef,$archiver,'-l',$part->full_name);
- for (undef $!; defined($ln=$proc_fh->getline); undef $!) {
- chomp($ln);
- next if $ln =~ /^(File size|----|Viewing cabinet:|\z)/;
- if ($ln !~ /^\s* (\d+) \s* \| [^|]* \| \s (.*) \z/x) {
- do_log(-1, "do_cabextract: can't parse toc line: $ln");
- } else {
- do_log(5, "do_cabextract: member: \"$2\", size: $1");
- $bytes += $1 if $1 > 0;
- }
- }
- defined $ln || $!==0 || $!==EAGAIN or die "Error reading: $!";
- # consume remaining output to avoid broken pipe (just in case)
- my($nbytes,$buff);
- while (($nbytes=$proc_fh->read($buff,4096)) > 0) { }
- defined $nbytes or die "Error reading: $!";
- my($err); $proc_fh->close or $err = $!;
- $?==0 or do_log(-1, 'cabextract-1 '.exit_status_str($?,$err));
- consumed_bytes($bytes, 'do_cabextract-pre', 1); # pre-check on estimated size
- mkdir("$tempdir/parts/cab", 0750) or die "Can't mkdir $tempdir/parts/cab: $!";
- ($proc_fh,$pid) = run_command(undef, '/dev/null', $archiver, '-q', '-d',
- "$tempdir/parts/cab", $part->full_name);
- my($output) = '';
- while (($nbytes=$proc_fh->read($buff,4096)) > 0) { $output .= $buff }
- defined $nbytes or die "Error reading: $!";
- $err = undef; $proc_fh->close or $err = $!;
- $?==0 or do_log(-1, 'cabextract-2 '.exit_status_str($?,$err).' '.$output);
- my($b) = flatten_and_tidy_dir("$tempdir/parts/cab", "$tempdir/parts", $part);
- consumed_bytes($b, 'do_cabextract');
- 1;
- }
- sub do_ole($$$) {
- my($part, $tempdir, $archiver) = @_;
- do_log(4,"Expanding MS OLE document " . $part->base_name);
- my($archiver_name) = basename((split(' ',$archiver))[0]);
- snmp_count("OpsDecBy\u${archiver_name}");
- mkdir("$tempdir/parts/ole",0750) or die "Can't mkdir $tempdir/parts/ole: $!";
- my($proc_fh,$pid) = run_command(undef, "&1", $archiver, '-v',
- '-i', $part->full_name, '-d',"$tempdir/parts/ole");
- my($nbytes,$buff); my($output) = '';
- while (($nbytes=$proc_fh->read($buff,4096)) > 0) { $output .= $buff }
- defined $nbytes or die "Error reading: $!";
- my($err); $proc_fh->close or $err = $!;
- $?==0 or do_log(0, 'ripOLE '.exit_status_str($?,$err).' '.$output);
- my($b) = flatten_and_tidy_dir("$tempdir/parts/ole", "$tempdir/parts", $part);
- if ($b > 0) {
- do_log(4, "ripOLE extracted $b bytes from an OLE document");
- consumed_bytes($b, 'do_ole');
- }
- 2; # always keep the original OLE document
- }
- # Check for self-extracting archives. Note that we don't rely on
- # file magic here since it's not reliable. Instead we will try each
- # archiver.
- sub do_executable($$@) {
- my($part, $tempdir, $unrar, $lha, $unarj) = @_;
- ll(4) && do_log(4,"Check whether ".$part->base_name.
- " is a self-extracting archive");
- # ZIP?
- return 2 if eval { do_unzip($part,$tempdir) };
- chomp($@);
- do_log(-1,"do_executable/do_unzip failed, ignoring: $@") if $@ ne '';
- # RAR?
- return 2 if defined $unrar && eval { do_unrar($part,$tempdir,$unrar) };
- chomp($@);
- do_log(-1,"do_executable/do_unrar failed, ignoring: $@") if $@ ne '';
- # LHA?
- return 2 if defined $lha && eval { do_lha($part,$tempdir,$lha) };
- chomp($@);
- do_log(-1,"do_executable/do_lha failed, ignoring: $@") if $@ ne '';
- # # ARJ?
- # return 2 if defined $unarj && eval { do_unarj($part,$tempdir,$unarj) };
- # chomp($@);
- # do_log(-1,"do_executable/do_unarj failed, ignoring: $@") if $@ ne '';
- return 0;
- }
- # my($k,$v,$fn);
- # while (($k,$v) = each(%::)) {
- # local(*e)=$v; $fn=fileno(\*e);
- # printf STDERR ("%-10s %-10s %s$eol",$k,$v,$fn) if defined $fn;
- # }
- # Given a file handle (typically opened pipe to a subprocess, as returned
- # from run_command), copy from it to a specified output file in binary mode.
- sub run_command_copy($$) {
- my($outfile, $ifh) = @_;
- my($ofh) = IO::File->new;
- $ofh->open($outfile, O_CREAT|O_EXCL|O_WRONLY, 0640)
- or die "Can't create file $outfile: $!";
- binmode($ofh) or die "Can't set file $outfile to binmode: $!";
- binmode($ifh) or die "Can't set binmode on pipe: $!";
- my($len, $buf, $offset, $written);
- for ($!=0; ($len=$ifh->sysread($buf,16384)) > 0; $!=0) {
- $offset = 0;
- while ($len > 0) { # handle partial writes
- $written = syswrite($ofh, $buf, $len, $offset);
- defined($written) or die "syswrite to $outfile failed: $!";
- consumed_bytes($written, 'run_command_copy');
- $len -= $written; $offset += $written;
- }
- }
- my($rv,$rerr); $rerr = 0;
- if (defined $len || $!==0) { $ifh->close or $rerr = $! } # ok
- else { $rerr = $!; $ifh->close } # remember error, ignore stat on close
- $rv = $?;
- $ofh->close or die "Error closing $outfile: $!";
- ($rv,$rerr); # return subprocess termination status and reading/close errno
- }
- # extract listed files from archive and store in new file
- sub store_mgr($$$@) {
- my($tempdir, $parent_obj, $list, $cmd, @args) = @_;
- my($item_num) = 0; my($parent_placement) = $parent_obj->mime_placement;
- my($result_status) = 0;
- for my $f (@$list) {
- next if $f =~ m{/\z}; # ignore directories
- my($newpart_obj) =
- Amavis::Unpackers::Part->new("$tempdir/parts",$parent_obj);
- $item_num++; $newpart_obj->mime_placement("$parent_placement/$item_num");
- $newpart_obj->name_declared($f); # store tainted name
- my($newpart) = $newpart_obj->full_name;
- ll(5) && do_log(5,sprintf('store_mgr: extracting "%s" to file %s using %s',
- $f, $newpart, $cmd));
- if ($f =~ m{^\.?[A-Za-z0-9_][A-Za-z0-9/._=~-]*\z}) { # apparently safe arg
- } else { # this is not too bad, as run_command does not use shell
- do_log(1, "store_mgr: NOTICE: untainting funny argument \"$f\"");
- }
- my($proc_fh,$pid) = run_command(undef,undef,$cmd,@args,untaint($f));
- my($rv,$rerr) = run_command_copy($newpart,$proc_fh);
- my($ll) = $rv!=0 || $rerr!= 0 ? 1 : 5;
- ll($ll) && do_log($ll,"store_mgr: extracted by $cmd, ".
- exit_status_str($rv,$rerr));
- $result_status = $rv if $result_status == 0 && $rv != 0;
- }
- $result_status; # return the first nonzero status (if any), or 0
- }
- 1;
- __DATA__
- #
- # =============================================================================
- # This text section governs how a main per-message amavisd-new log entry
- # is formed. An empty text will prevent a log entry, multi-line text will
- # produce several log entries, one for each nonempty line.
- # Syntax is explained in the README.customize file.
- [?%#D|#|Passed #
- [? [?%#V|1] |INFECTED (%V)|#
- [? [?%#F|1] |BANNED (%F)|#
- [? [? %2|1] |SPAM|#
- [? [?%#X|1] |BAD-HEADER|CLEAN]]]]#
- , [? %p ||%p ][?%a||[?%l||LOCAL ]\[%a\] ][?%e||\[%e\] ]<%o> -> [%D|,]#
- [? %q ||, quarantine: %q]#
- [? %Q ||, Queue-ID: %Q]#
- [? %m ||, Message-ID: %m]#
- [? %r ||, Resent-Message-ID: %r]#
- , mail_id: %i#
- , Hits: %c#
- #, size: %z#
- #[? %j ||, Subject: "%j"]#
- #[? %#T ||, Tests: \[[%T|,]]\]#
- , %y ms#
- ]
- [?%#O|#|Blocked #
- [? [?%#V|1] |INFECTED (%V)|#
- [? [?%#F|1] |BANNED (%F)|#
- [? [? %2|1] |SPAM|#
- [? [?%#X|1] |BAD-HEADER|CLEAN]]]]#
- , [? %p ||%p ][?%a||[?%l||LOCAL ]\[%a\] ][?%e||\[%e\] ]<%o> -> [%O|,]#
- [? %q ||, quarantine: %q]#
- [? %Q ||, Queue-ID: %Q]#
- [? %m ||, Message-ID: %m]#
- [? %r ||, Resent-Message-ID: %r]#
- , mail_id: %i#
- , Hits: %c#
- #, size: %z#
- #[? %j ||, Subject: "%j"]#
- #[? %#T ||, Tests: \[[%T|,]]\]#
- , %y ms#
- ]
- __DATA__
- #
- # =============================================================================
- # This text section governs how a main per-recipient amavisd-new log entry
- # is formed. An empty text will prevent a log entry, multi-line text will
- # produce several log entries, one for each nonempty line.
- # Macro %. might be useful, it counts recipients starting from 1.
- # Syntax is explained in the README.customize file.
- #
- [?%#D|#|Passed #
- [? [?%#V|1] |INFECTED (%V)|#
- [? [?%#F|1] |BANNED (%F)|#
- [? [? %2|1] |SPAM|#
- [? [?%#X|1] |BAD-HEADER|CLEAN]]]]#
- , <%o> -> [%D|,], Hits: %c#
- , tag=%3, tag2=%4, kill=%5# NOTE: macros %3, %4, %5 are experimental
- , %0/%1/%2/%k#
- ]
- [?%#O|#|Blocked #
- [? [?%#V|1] |INFECTED (%V)|#
- [? [?%#F|1] |BANNED (%F)|#
- [? [? %2|1] |SPAM|#
- [? [?%#X|1] |BAD-HEADER|CLEAN]]]]#
- , <%o> -> [%O|,], Hits: %c#
- , tag=%3, tag2=%4, kill=%5# NOTE: macros %3, %4, %5 are experimental
- , %0/%1/%2/%k#
- ]
- __DATA__
- #
- # =============================================================================
- # This is a template for (neutral: non-virus, non-spam, non-banned) DELIVERY
- # STATUS NOTIFICATIONS to sender. For syntax and customization instructions
- # see README.customize. Note that only valid header fields are allowed;
- # non-standard header field heads must begin with "X-" .
- # The From, To and Date header fields will be provided automatically.
- #
- Subject: [?%#D|Undeliverable mail|Delivery warning][?%#X||, invalid characters in header]
- Message-ID: <DSN%i@%h>
- [? %#X ||INVALID HEADER (INVALID CHARACTERS OR SPACE GAP)
- [%X\n]
- ]\
- This [?%#D|nondelivery|delivery] report was generated by the amavisd-new program
- at host %h. Our internal reference code for your message
- is %n/%i.
- [? %#X ||
- WHAT IS AN INVALID CHARACTER IN MAIL HEADER?
- The RFC 2822 standard specifies rules for forming internet messages.
- It does not allow the use of characters with codes above 127 to be used
- directly (non-encoded) in mail header (it also prohibits NUL and bare CR).
- If characters (e.g. with diacritics) from ISO Latin or other alphabets
- need to be included in the header, these characters need to be properly
- encoded according to RFC 2047. This encoding is often done transparently
- by mail reader (MUA), but if automatic encoding is not available (e.g.
- by some older MUA) it is the user's responsibility to avoid the use
- of such characters in mail header, or to encode them manually. Typically
- the offending header fields in this category are 'Subject', 'Organization',
- and comment fields in e-mail addresses of the 'From', 'To' and 'Cc'.
- Sometimes such invalid header fields are inserted automatically
- by some MUA, MTA, content checker, or other mail handling service.
- If this is the case, that service needs to be fixed or properly configured.
- Typically the offending header fields in this category are 'Date',
- 'Received', 'X-Mailer', 'X-Priority', 'X-Scanned', etc.
- If you don't know how to fix or avoid the problem, please report it
- to _your_ postmaster or system manager.
- ]\
- Return-Path: %s
- Your message[?%m|| %m][?%r|| (Resent-Message-ID: %r)]
- [?%#D|could not be|was] delivered to:[\n %N]
- __DATA__
- #
- # =============================================================================
- # This is a template for VIRUS/BANNED SENDER NOTIFICATIONS.
- # For syntax and customization instructions see README.customize.
- # Note that only valid header fields are allowed;
- # non-standard header field heads must begin with "X-" .
- # The From, To and Date header fields will be provided automatically.
- #
- Subject: [? %#V |[? %#F |Unknown problem|BANNED (%F)]|VIRUS (%V)] IN MAIL FROM YOU
- [? %m |#|In-Reply-To: %m]
- Message-ID: <VS%i@%h>
- [? %#V |[? %#F |[? %#X ||INVALID HEADER]|BANNED CONTENTS ALERT]|VIRUS ALERT]
- Our content checker found
- [? %#V |#| [? %#V |viruses|virus|viruses]: %V]
- [? %#F |#| banned [? %#F |names|name|names]: %F]
- [? %#X |#|\n[%X\n]]
- in email presumably from you (%s),
- to the following [? %#R |recipients|recipient|recipients]:[
- -> %R]
- [? %a |#|First upstream SMTP client IP address: \[%a\] %g]
- [? %e |#|According to the 'Received:' trace, the message originated at: \[%e\]]
- Our internal reference code for your message is %n/%i.
- [? %#V ||Please check your system for viruses,
- or ask your system administrator to do so.
- ]#
- [? %#D |Delivery of the email was stopped!
- ]#
- [? %#V |[? %#F ||#
- The message [?%#D|has been blocked|triggered this warning] because it contains a component
- (as a MIME part or nested within) with declared name
- or MIME type or contents type violating our access policy.
- To transfer contents that may be considered risky or unwanted
- by site policies, or simply too large for mailing, please consider
- publishing your content on the web, and only sending an URL of the
- document to the recipient.
- Depending on the recipient and sender site policies, with a little
- effort it might still be possible to send any contents (including
- viruses) using one of the following methods:
- - encrypted using pgp, gpg or other encryption methods;
- - wrapped in a password-protected or scrambled container or archive
- (e.g.: zip -e, arj -g, arc g, rar -p, or other methods)
- Note that if the contents is not intended to be secret, the
- encryption key or password may be included in the same message
- for recipient's convenience.
- We are sorry for inconvenience if the contents was not malicious.
- The purpose of these restrictions is to cut the most common propagation
- methods used by viruses and other malware. These often exploit automatic
- mechanisms and security holes in more popular mail readers (Microsoft
- mail readers and browsers are a common target). By requiring an explicit
- and decisive action from the recipient to decode mail, the dangers of
- automatic malware propagation is largely reduced.
- #
- # Details of our mail restrictions policy are available at ...
- ]]#
- For your reference, here are headers from your email:
- ------------------------- BEGIN HEADERS -----------------------------
- Return-Path: %s
- [%H
- ]\
- -------------------------- END HEADERS ------------------------------
- __DATA__
- #
- # =============================================================================
- # This is a template for non-spam (VIRUS,...) ADMINISTRATOR NOTIFICATIONS.
- # For syntax and customization instructions see README.customize.
- # Note that only valid header fields are allowed; non-standard header
- # field heads must begin with "X-" .
- #
- Date: %d
- From: %f
- Subject: [? %#V |[? %#F |[? %#X ||INVALID HEADER]|BANNED (%F)]|VIRUS (%V)]#
- FROM [?%l||LOCAL ][?%a||\[%a\] ][?%o|(?)|<%o>]
- To: [? %#T |undisclosed-recipients: ;|[<%T>|, ]]
- [? %#C |#|Cc: [<%C>|, ]]
- Message-ID: <VA%i@%h>
- [? %#V |No viruses were found.
- |A virus was found: %V
- |Two viruses were found:\n %V
- |%#V viruses were found:\n %V
- ]
- [? %#F |#\
- |A banned name was found:\n %F
- |Two banned names were found:\n %F
- |%#F banned names were found:\n %F
- ]
- [? %#X |#\
- |Bad header was found:[\n %X]
- ]
- [? %#W |#\
- |Scanner detecting a virus: %W
- |Scanners detecting a virus: %W
- ]
- Our internal reference code for the message is %n/%i.
- The mail originated from: <%o>
- [? %a |#|First upstream SMTP client IP address: \[%a\] %g
- ]
- [? %t |#|According to the 'Received:' trace, the message originated at:
- \[%e\]
- %t
- ]
- [? %#S |Notification to sender will not be mailed.
- ]#
- [? %#D |#|The message WILL BE delivered to:[\n%D]
- ]
- [? %#N |#|The message WAS NOT delivered to:[\n%N]
- ]
- [? %#V |#|[? %#v |#|Virus scanner output:[\n %v]
- ]]
- [? %q |Not quarantined.|The message has been quarantined as:\n %q
- ]
- ------------------------- BEGIN HEADERS -----------------------------
- Return-Path: %s
- [%H
- ]\
- -------------------------- END HEADERS ------------------------------
- __DATA__
- #
- # =============================================================================
- # This is a template for VIRUS/BANNED/BAD-HEADER RECIPIENTS NOTIFICATIONS.
- # For syntax and customization instructions see README.customize.
- # Note that only valid header fields are allowed; non-standard header
- # field heads must begin with "X-" .
- #
- Date: %d
- From: %f
- Subject: [? %#V |[? %#F |[? %#X ||INVALID HEADER]|BANNED]|VIRUS (%V)]#
- IN MAIL TO YOU (from [?%o|(?)|<%o>])
- To: [? %#T |undisclosed-recipients: ;|[<%T>|, ]]
- [? %#C |#|Cc: [<%C>|, ]]
- Message-ID: <VR%i@%h>
- [? %#V |[? %#F ||BANNED CONTENTS ALERT]|VIRUS ALERT]
- Our content checker found
- [? %#V |#| [? %#V |viruses|virus|viruses]: %V]
- [? %#F |#| banned [? %#F |names|name|names]: %F]
- [? %#X |#|\n[%X\n]]
- in an email to you [? %S |from unknown sender:|from:]
- %o
- [? %S |claiming to be: %s|#]
- [? %a |#|First upstream SMTP client IP address: \[%a\] %g
- ]
- [? %t |#|According to the 'Received:' trace, the message originated at:
- \[%e\]
- %t
- ]
- Our internal reference code for the message is %n/%i.
- [? %q |Not quarantined.|The message has been quarantined as:
- %q]
- Please contact your system administrator for details.
- __DATA__
- #
- # =============================================================================
- # This is a template for SPAM SENDER NOTIFICATIONS.
- # For syntax and customization instructions see README.customize.
- # Note that only valid header fields are allowed;
- # non-standard header field heads must begin with "X-" .
- # The From, To and Date header fields will be provided automatically.
- #
- Subject: Considered UNSOLICITED BULK EMAIL from you
- [? %m |#|In-Reply-To: %m]
- Message-ID: <SS%i@%h>
- Your message to:[
- -> %R]
- was considered unsolicited bulk e-mail (UBE).
- [? %#X |#|\n[%X\n]]
- Subject: %j
- Return-Path: %s
- [? %a |#|First upstream SMTP client IP address: \[%a\] %g]
- [? %e |#|According to the 'Received:' trace, the message originated at: \[%e\]]
- Our internal reference code for your message is %n/%i.
- [? %#D |Delivery of the email was stopped!
- ]#
- #
- # SpamAssassin report:
- # [%A
- # ]\
- __DATA__
- #
- # =============================================================================
- # This is a template for SPAM ADMINISTRATOR NOTIFICATIONS.
- # For syntax and customization instructions see README.customize.
- # Note that only valid header fields are allowed; non-standard header
- # field heads must begin with "X-" .
- #
- Date: %d
- From: %f
- Subject: SPAM FROM [?%l||LOCAL ][?%a||\[%a\] ][?%o|(?)|<%o>]
- To: [? %#T |undisclosed-recipients: ;|[<%T>|, ]]
- [? %#C |#|Cc: [<%C>|, ]]
- [? %#B |#|Bcc: [<%B>|, ]]
- Message-ID: <SA%i@%h>
- Unsolicited bulk email [? %S |from unknown or forged sender:|from:]
- %o
- Subject: %j
- Our internal reference code for the message is %n/%i.
- [? %a |#|First upstream SMTP client IP address: \[%a\] %g
- ]
- [? %t |#|According to the 'Received:' trace, the message originated at:
- \[%e\]
- %t
- ]
- [? %#D |#|The message WILL BE delivered to:[\n%D]
- ]
- [? %#N |#|The message WAS NOT delivered to:[\n%N]
- ]
- [? %q |Not quarantined.|The message has been quarantined as:\n %q
- ]
- SpamAssassin report:
- [%A
- ]\
- ------------------------- BEGIN HEADERS -----------------------------
- Return-Path: %s
- [%H
- ]\
- -------------------------- END HEADERS ------------------------------
|