test_imap.py 179 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280428142824283428442854286428742884289429042914292429342944295429642974298429943004301430243034304430543064307430843094310431143124313431443154316431743184319432043214322432343244325432643274328432943304331433243334334433543364337433843394340434143424343434443454346434743484349435043514352435343544355435643574358435943604361436243634364436543664367436843694370437143724373437443754376437743784379438043814382438343844385438643874388438943904391439243934394439543964397439843994400440144024403440444054406440744084409441044114412441344144415441644174418441944204421442244234424442544264427442844294430443144324433443444354436443744384439444044414442444344444445444644474448444944504451445244534454445544564457445844594460446144624463446444654466446744684469447044714472447344744475447644774478447944804481448244834484448544864487448844894490449144924493449444954496449744984499450045014502450345044505450645074508450945104511451245134514451545164517451845194520452145224523452445254526452745284529453045314532453345344535453645374538453945404541454245434544454545464547454845494550455145524553455445554556455745584559456045614562456345644565456645674568456945704571457245734574457545764577457845794580458145824583458445854586458745884589459045914592459345944595459645974598459946004601460246034604460546064607460846094610461146124613461446154616461746184619462046214622462346244625462646274628462946304631463246334634463546364637463846394640464146424643464446454646464746484649465046514652465346544655465646574658465946604661466246634664466546664667466846694670467146724673467446754676467746784679468046814682468346844685468646874688468946904691469246934694469546964697469846994700470147024703470447054706470747084709471047114712471347144715471647174718471947204721472247234724472547264727472847294730473147324733473447354736473747384739474047414742474347444745474647474748474947504751475247534754475547564757475847594760476147624763476447654766476747684769477047714772477347744775477647774778477947804781478247834784478547864787478847894790479147924793479447954796479747984799480048014802480348044805480648074808480948104811481248134814481548164817481848194820482148224823482448254826482748284829483048314832483348344835483648374838483948404841484248434844484548464847484848494850485148524853485448554856485748584859486048614862486348644865486648674868486948704871487248734874487548764877487848794880488148824883488448854886488748884889489048914892489348944895489648974898489949004901490249034904490549064907490849094910491149124913491449154916491749184919492049214922492349244925492649274928492949304931493249334934493549364937493849394940494149424943494449454946494749484949495049514952495349544955495649574958495949604961496249634964496549664967496849694970497149724973497449754976497749784979498049814982498349844985498649874988498949904991499249934994499549964997499849995000500150025003500450055006500750085009501050115012501350145015501650175018501950205021502250235024502550265027502850295030503150325033503450355036503750385039504050415042504350445045504650475048504950505051505250535054505550565057505850595060506150625063506450655066506750685069507050715072507350745075507650775078507950805081508250835084508550865087508850895090509150925093509450955096509750985099510051015102510351045105510651075108510951105111511251135114511551165117511851195120512151225123512451255126512751285129513051315132513351345135513651375138513951405141514251435144514551465147514851495150515151525153515451555156515751585159516051615162516351645165516651675168516951705171517251735174517551765177517851795180518151825183518451855186518751885189519051915192519351945195519651975198519952005201520252035204520552065207520852095210521152125213521452155216521752185219522052215222522352245225522652275228522952305231523252335234523552365237523852395240524152425243524452455246524752485249525052515252525352545255525652575258525952605261526252635264526552665267
  1. # -*- test-case-name: twisted.mail.test.test_imap -*-
  2. # Copyright (c) Twisted Matrix Laboratories.
  3. # See LICENSE for details.
  4. """
  5. Test case for twisted.mail.imap4
  6. """
  7. import codecs
  8. import locale
  9. import os
  10. from io import BytesIO
  11. from itertools import chain
  12. from collections import OrderedDict
  13. from zope.interface import implementer
  14. from twisted.internet import defer
  15. from twisted.internet import error
  16. from twisted.internet import interfaces
  17. from twisted.internet import reactor
  18. from twisted.internet.task import Clock
  19. from twisted.mail import imap4
  20. from twisted.mail.imap4 import MessageSet
  21. from twisted.protocols import loopback
  22. from twisted.python import failure
  23. from twisted.python import util, log
  24. from twisted.python.compat import intToBytes, networkString, range
  25. from twisted.trial import unittest
  26. from twisted.cred.portal import Portal
  27. from twisted.cred.checkers import InMemoryUsernamePasswordDatabaseDontUse
  28. from twisted.cred.error import UnauthorizedLogin
  29. from twisted.cred.credentials import (
  30. IUsernameHashedPassword, IUsernamePassword, CramMD5Credentials)
  31. from twisted.test.proto_helpers import StringTransport, StringTransportWithDisconnection
  32. try:
  33. from twisted.test.ssl_helpers import ClientTLSContext, ServerTLSContext
  34. except ImportError:
  35. ClientTLSContext = ServerTLSContext = None
  36. def strip(f):
  37. return lambda result, f=f: f()
  38. def sortNest(l):
  39. l = l[:]
  40. l.sort()
  41. for i in range(len(l)):
  42. if isinstance(l[i], list):
  43. l[i] = sortNest(l[i])
  44. elif isinstance(l[i], tuple):
  45. l[i] = tuple(sortNest(list(l[i])))
  46. return l
  47. class IMAP4UTF7Tests(unittest.TestCase):
  48. tests = [
  49. [u'Hello world', b'Hello world'],
  50. [u'Hello & world', b'Hello &- world'],
  51. [u'Hello\xffworld', b'Hello&AP8-world'],
  52. [u'\xff\xfe\xfd\xfc', b'&AP8A,gD9APw-'],
  53. [u'~peter/mail/\u65e5\u672c\u8a9e/\u53f0\u5317',
  54. b'~peter/mail/&ZeVnLIqe-/&U,BTFw-'], # example from RFC 2060
  55. ]
  56. def test_encodeWithErrors(self):
  57. """
  58. Specifying an error policy to C{unicode.encode} with the
  59. I{imap4-utf-7} codec should produce the same result as not
  60. specifying the error policy.
  61. """
  62. text = u'Hello world'
  63. self.assertEqual(
  64. text.encode('imap4-utf-7', 'strict'),
  65. text.encode('imap4-utf-7'))
  66. def test_decodeWithErrors(self):
  67. """
  68. Similar to L{test_encodeWithErrors}, but for C{bytes.decode}.
  69. """
  70. bytes = b'Hello world'
  71. self.assertEqual(
  72. bytes.decode('imap4-utf-7', 'strict'),
  73. bytes.decode('imap4-utf-7'))
  74. def test_getreader(self):
  75. """
  76. C{codecs.getreader('imap4-utf-7')} returns the I{imap4-utf-7} stream
  77. reader class.
  78. """
  79. reader = codecs.getreader('imap4-utf-7')(BytesIO(b'Hello&AP8-world'))
  80. self.assertEqual(reader.read(), u'Hello\xffworld')
  81. def test_getwriter(self):
  82. """
  83. C{codecs.getwriter('imap4-utf-7')} returns the I{imap4-utf-7} stream
  84. writer class.
  85. """
  86. output = BytesIO()
  87. writer = codecs.getwriter('imap4-utf-7')(output)
  88. writer.write(u'Hello\xffworld')
  89. self.assertEqual(output.getvalue(), b'Hello&AP8-world')
  90. def test_encode(self):
  91. """
  92. The I{imap4-utf-7} can be used to encode a unicode string into a byte
  93. string according to the IMAP4 modified UTF-7 encoding rules.
  94. """
  95. for (input, output) in self.tests:
  96. self.assertEqual(input.encode('imap4-utf-7'), output)
  97. def test_decode(self):
  98. """
  99. The I{imap4-utf-7} can be used to decode a byte string into a unicode
  100. string according to the IMAP4 modified UTF-7 encoding rules.
  101. """
  102. for (input, output) in self.tests:
  103. self.assertEqual(input, output.decode('imap4-utf-7'))
  104. def test_printableSingletons(self):
  105. """
  106. The IMAP4 modified UTF-7 implementation encodes all printable
  107. characters which are in ASCII using the corresponding ASCII byte.
  108. """
  109. # All printables represent themselves
  110. for o in chain(range(0x20, 0x26), range(0x27, 0x7f)):
  111. charbyte = chr(o).encode()
  112. self.assertEqual(charbyte, chr(o).encode('imap4-utf-7'))
  113. self.assertEqual(chr(o), charbyte.decode('imap4-utf-7'))
  114. self.assertEqual(u'&'.encode('imap4-utf-7'), b'&-')
  115. self.assertEqual(b'&-'.decode('imap4-utf-7'), u'&')
  116. class BufferingConsumer:
  117. def __init__(self):
  118. self.buffer = []
  119. def write(self, bytes):
  120. self.buffer.append(bytes)
  121. if self.consumer:
  122. self.consumer.resumeProducing()
  123. def registerProducer(self, consumer, streaming):
  124. self.consumer = consumer
  125. self.consumer.resumeProducing()
  126. def unregisterProducer(self):
  127. self.consumer = None
  128. class MessageProducerTests(unittest.TestCase):
  129. def testSinglePart(self):
  130. body = b'This is body text. Rar.'
  131. headers = OrderedDict()
  132. headers[b'from'] = b'sender@host'
  133. headers[b'to'] = b'recipient@domain'
  134. headers[b'subject'] = b'booga booga boo'
  135. headers[b'content-type'] = b'text/plain'
  136. msg = FakeyMessage(headers, (), None, body, 123, None )
  137. c = BufferingConsumer()
  138. p = imap4.MessageProducer(msg)
  139. d = p.beginProducing(c)
  140. def cbProduced(result):
  141. self.assertIdentical(result, p)
  142. self.assertEqual(
  143. b''.join(c.buffer),
  144. b'{119}\r\n'
  145. b'From: sender@host\r\n'
  146. b'To: recipient@domain\r\n'
  147. b'Subject: booga booga boo\r\n'
  148. b'Content-Type: text/plain\r\n'
  149. b'\r\n'
  150. + body)
  151. return d.addCallback(cbProduced)
  152. def testSingleMultiPart(self):
  153. outerBody = b''
  154. innerBody = b'Contained body message text. Squarge.'
  155. headers = OrderedDict()
  156. headers[b'from'] = b'sender@host'
  157. headers[b'to'] = b'recipient@domain'
  158. headers[b'subject'] = b'booga booga boo'
  159. headers[b'content-type'] = b'multipart/alternative; boundary="xyz"'
  160. innerHeaders = OrderedDict()
  161. innerHeaders[b'subject'] = b'this is subject text'
  162. innerHeaders[b'content-type'] = b'text/plain'
  163. msg = FakeyMessage(headers, (), None, outerBody, 123,
  164. [FakeyMessage(innerHeaders, (), None, innerBody,
  165. None, None)],
  166. )
  167. c = BufferingConsumer()
  168. p = imap4.MessageProducer(msg)
  169. d = p.beginProducing(c)
  170. def cbProduced(result):
  171. self.failUnlessIdentical(result, p)
  172. self.assertEqual(
  173. b''.join(c.buffer),
  174. b'{239}\r\n'
  175. b'From: sender@host\r\n'
  176. b'To: recipient@domain\r\n'
  177. b'Subject: booga booga boo\r\n'
  178. b'Content-Type: multipart/alternative; boundary="xyz"\r\n'
  179. b'\r\n'
  180. b'\r\n'
  181. b'--xyz\r\n'
  182. b'Subject: this is subject text\r\n'
  183. b'Content-Type: text/plain\r\n'
  184. b'\r\n'
  185. + innerBody
  186. + b'\r\n--xyz--\r\n')
  187. return d.addCallback(cbProduced)
  188. def testMultipleMultiPart(self):
  189. outerBody = b''
  190. innerBody1 = b'Contained body message text. Squarge.'
  191. innerBody2 = b'Secondary <i>message</i> text of squarge body.'
  192. headers = OrderedDict()
  193. headers[b'from'] = b'sender@host'
  194. headers[b'to'] = b'recipient@domain'
  195. headers[b'subject'] = b'booga booga boo'
  196. headers[b'content-type'] = b'multipart/alternative; boundary="xyz"'
  197. innerHeaders = OrderedDict()
  198. innerHeaders[b'subject'] = b'this is subject text'
  199. innerHeaders[b'content-type'] = b'text/plain'
  200. innerHeaders2 = OrderedDict()
  201. innerHeaders2[b'subject'] = b'<b>this is subject</b>'
  202. innerHeaders2[b'content-type'] = b'text/html'
  203. msg = FakeyMessage(headers, (), None, outerBody, 123, [
  204. FakeyMessage(innerHeaders, (), None, innerBody1, None, None),
  205. FakeyMessage(innerHeaders2, (), None, innerBody2, None, None)
  206. ],
  207. )
  208. c = BufferingConsumer()
  209. p = imap4.MessageProducer(msg)
  210. d = p.beginProducing(c)
  211. def cbProduced(result):
  212. self.failUnlessIdentical(result, p)
  213. self.assertEqual(
  214. b''.join(c.buffer),
  215. b'{354}\r\n'
  216. b'From: sender@host\r\n'
  217. b'To: recipient@domain\r\n'
  218. b'Subject: booga booga boo\r\n'
  219. b'Content-Type: multipart/alternative; boundary="xyz"\r\n'
  220. b'\r\n'
  221. b'\r\n'
  222. b'--xyz\r\n'
  223. b'Subject: this is subject text\r\n'
  224. b'Content-Type: text/plain\r\n'
  225. b'\r\n'
  226. + innerBody1
  227. + b'\r\n--xyz\r\n'
  228. b'Subject: <b>this is subject</b>\r\n'
  229. b'Content-Type: text/html\r\n'
  230. b'\r\n'
  231. + innerBody2
  232. + b'\r\n--xyz--\r\n')
  233. return d.addCallback(cbProduced)
  234. class IMAP4HelperTests(unittest.TestCase):
  235. """
  236. Tests for various helper utilities in the IMAP4 module.
  237. """
  238. def test_fileProducer(self):
  239. b = (('x' * 1) + ('y' * 1) + ('z' * 1)) * 10
  240. c = BufferingConsumer()
  241. f = BytesIO(b)
  242. p = imap4.FileProducer(f)
  243. d = p.beginProducing(c)
  244. def cbProduced(result):
  245. self.failUnlessIdentical(result, p)
  246. self.assertEqual(
  247. ('{%d}\r\n' % len(b))+ b,
  248. ''.join(c.buffer))
  249. return d.addCallback(cbProduced)
  250. def test_wildcard(self):
  251. cases = [
  252. ['foo/%gum/bar',
  253. ['foo/bar', 'oo/lalagum/bar', 'foo/gumx/bar', 'foo/gum/baz'],
  254. ['foo/xgum/bar', 'foo/gum/bar'],
  255. ], ['foo/x%x/bar',
  256. ['foo', 'bar', 'fuz fuz fuz', 'foo/*/bar', 'foo/xyz/bar', 'foo/xx/baz'],
  257. ['foo/xyx/bar', 'foo/xx/bar', 'foo/xxxxxxxxxxxxxx/bar'],
  258. ], ['foo/xyz*abc/bar',
  259. ['foo/xyz/bar', 'foo/abc/bar', 'foo/xyzab/cbar', 'foo/xyza/bcbar'],
  260. ['foo/xyzabc/bar', 'foo/xyz/abc/bar', 'foo/xyz/123/abc/bar'],
  261. ]
  262. ]
  263. for (wildcard, fail, succeed) in cases:
  264. wildcard = imap4.wildcardToRegexp(wildcard, '/')
  265. for x in fail:
  266. self.assertFalse(wildcard.match(x))
  267. for x in succeed:
  268. self.assertTrue(wildcard.match(x))
  269. def test_wildcardNoDelim(self):
  270. cases = [
  271. ['foo/%gum/bar',
  272. ['foo/bar', 'oo/lalagum/bar', 'foo/gumx/bar', 'foo/gum/baz'],
  273. ['foo/xgum/bar', 'foo/gum/bar', 'foo/x/gum/bar'],
  274. ], ['foo/x%x/bar',
  275. ['foo', 'bar', 'fuz fuz fuz', 'foo/*/bar', 'foo/xyz/bar', 'foo/xx/baz'],
  276. ['foo/xyx/bar', 'foo/xx/bar', 'foo/xxxxxxxxxxxxxx/bar', 'foo/x/x/bar'],
  277. ], ['foo/xyz*abc/bar',
  278. ['foo/xyz/bar', 'foo/abc/bar', 'foo/xyzab/cbar', 'foo/xyza/bcbar'],
  279. ['foo/xyzabc/bar', 'foo/xyz/abc/bar', 'foo/xyz/123/abc/bar'],
  280. ]
  281. ]
  282. for (wildcard, fail, succeed) in cases:
  283. wildcard = imap4.wildcardToRegexp(wildcard, None)
  284. for x in fail:
  285. self.assertFalse(wildcard.match(x), x)
  286. for x in succeed:
  287. self.assertTrue(wildcard.match(x), x)
  288. def test_headerFormatter(self):
  289. """
  290. L{imap4._formatHeaders} accepts a C{dict} of header name/value pairs and
  291. returns a string representing those headers in the standard multiline,
  292. C{":"}-separated format.
  293. """
  294. cases = [
  295. ({'Header1': 'Value1', 'Header2': 'Value2'}, 'Header2: Value2\r\nHeader1: Value1\r\n'),
  296. ]
  297. for (input, expected) in cases:
  298. output = imap4._formatHeaders(input)
  299. self.assertEqual(sorted(output.splitlines(True)),
  300. sorted(expected.splitlines(True)))
  301. def test_messageSet(self):
  302. m1 = MessageSet()
  303. m2 = MessageSet()
  304. self.assertEqual(m1, m2)
  305. m1 = m1 + (1, 3)
  306. self.assertEqual(len(m1), 3)
  307. self.assertEqual(list(m1), [1, 2, 3])
  308. m2 = m2 + (1, 3)
  309. self.assertEqual(m1, m2)
  310. self.assertEqual(list(m1 + m2), [1, 2, 3])
  311. def test_messageSetStringRepresentationWithWildcards(self):
  312. """
  313. In a L{MessageSet}, in the presence of wildcards, if the highest message
  314. id is known, the wildcard should get replaced by that high value.
  315. """
  316. inputs = [
  317. MessageSet(imap4.parseIdList(b'*')),
  318. MessageSet(imap4.parseIdList(b'3:*', 6)),
  319. MessageSet(imap4.parseIdList(b'*:2', 6)),
  320. ]
  321. outputs = [
  322. b"*",
  323. b"3:6",
  324. b"2:6",
  325. ]
  326. for i, o in zip(inputs, outputs):
  327. self.assertEqual(str(i), o)
  328. def test_messageSetStringRepresentationWithInversion(self):
  329. """
  330. In a L{MessageSet}, inverting the high and low numbers in a range
  331. doesn't affect the meaning of the range. For example, 3:2 displays just
  332. like 2:3, because according to the RFC they have the same meaning.
  333. """
  334. inputs = [
  335. MessageSet(imap4.parseIdList(b'2:3')),
  336. MessageSet(imap4.parseIdList(b'3:2')),
  337. ]
  338. outputs = [
  339. "2:3",
  340. "2:3",
  341. ]
  342. for i, o in zip(inputs, outputs):
  343. self.assertEqual(str(i), o)
  344. def test_quotedSplitter(self):
  345. cases = [
  346. '''Hello World''',
  347. '''Hello "World!"''',
  348. '''World "Hello" "How are you?"''',
  349. '''"Hello world" How "are you?"''',
  350. '''foo bar "baz buz" NIL''',
  351. '''foo bar "baz buz" "NIL"''',
  352. '''foo NIL "baz buz" bar''',
  353. '''foo "NIL" "baz buz" bar''',
  354. '''"NIL" bar "baz buz" foo''',
  355. 'oo \\"oo\\" oo',
  356. '"oo \\"oo\\" oo"',
  357. 'oo \t oo',
  358. '"oo \t oo"',
  359. 'oo \\t oo',
  360. '"oo \\t oo"',
  361. 'oo \o oo',
  362. '"oo \o oo"',
  363. 'oo \\o oo',
  364. '"oo \\o oo"',
  365. ]
  366. answers = [
  367. ['Hello', 'World'],
  368. ['Hello', 'World!'],
  369. ['World', 'Hello', 'How are you?'],
  370. ['Hello world', 'How', 'are you?'],
  371. ['foo', 'bar', 'baz buz', None],
  372. ['foo', 'bar', 'baz buz', 'NIL'],
  373. ['foo', None, 'baz buz', 'bar'],
  374. ['foo', 'NIL', 'baz buz', 'bar'],
  375. ['NIL', 'bar', 'baz buz', 'foo'],
  376. ['oo', '"oo"', 'oo'],
  377. ['oo "oo" oo'],
  378. ['oo', 'oo'],
  379. ['oo \t oo'],
  380. ['oo', '\\t', 'oo'],
  381. ['oo \\t oo'],
  382. ['oo', '\o', 'oo'],
  383. ['oo \o oo'],
  384. ['oo', '\\o', 'oo'],
  385. ['oo \\o oo'],
  386. ]
  387. errors = [
  388. '"mismatched quote',
  389. 'mismatched quote"',
  390. 'mismatched"quote',
  391. '"oops here is" another"',
  392. ]
  393. for s in errors:
  394. self.assertRaises(imap4.MismatchedQuoting, imap4.splitQuoted, s)
  395. for (case, expected) in zip(cases, answers):
  396. self.assertEqual(imap4.splitQuoted(case), expected)
  397. def test_stringCollapser(self):
  398. cases = [
  399. ['a', 'b', 'c', 'd', 'e'],
  400. ['a', ' ', '"', 'b', 'c', ' ', '"', ' ', 'd', 'e'],
  401. [['a', 'b', 'c'], 'd', 'e'],
  402. ['a', ['b', 'c', 'd'], 'e'],
  403. ['a', 'b', ['c', 'd', 'e']],
  404. ['"', 'a', ' ', '"', ['b', 'c', 'd'], '"', ' ', 'e', '"'],
  405. ['a', ['"', ' ', 'b', 'c', ' ', ' ', '"'], 'd', 'e'],
  406. ]
  407. answers = [
  408. ['abcde'],
  409. ['a', 'bc ', 'de'],
  410. [['abc'], 'de'],
  411. ['a', ['bcd'], 'e'],
  412. ['ab', ['cde']],
  413. ['a ', ['bcd'], ' e'],
  414. ['a', [' bc '], 'de'],
  415. ]
  416. for (case, expected) in zip(cases, answers):
  417. self.assertEqual(imap4.collapseStrings(case), expected)
  418. def test_parenParser(self):
  419. s = '\r\n'.join(['xx'] * 4)
  420. cases = [
  421. '(BODY.PEEK[HEADER.FIELDS.NOT (subject bcc cc)] {%d}\r\n%s)' % (len(s), s,),
  422. # '(FLAGS (\Seen) INTERNALDATE "17-Jul-1996 02:44:25 -0700" '
  423. # 'RFC822.SIZE 4286 ENVELOPE ("Wed, 17 Jul 1996 02:23:25 -0700 (PDT)" '
  424. # '"IMAP4rev1 WG mtg summary and minutes" '
  425. # '(("Terry Gray" NIL "gray" "cac.washington.edu")) '
  426. # '(("Terry Gray" NIL "gray" "cac.washington.edu")) '
  427. # '(("Terry Gray" NIL "gray" "cac.washington.edu")) '
  428. # '((NIL NIL "imap" "cac.washington.edu")) '
  429. # '((NIL NIL "minutes" "CNRI.Reston.VA.US") '
  430. # '("John Klensin" NIL "KLENSIN" "INFOODS.MIT.EDU")) NIL NIL '
  431. # '"<B27397-0100000@cac.washington.edu>") '
  432. # 'BODY ("TEXT" "PLAIN" ("CHARSET" "US-ASCII") NIL NIL "7BIT" 3028 92))',
  433. '(FLAGS (\Seen) INTERNALDATE "17-Jul-1996 02:44:25 -0700" '
  434. 'RFC822.SIZE 4286 ENVELOPE ("Wed, 17 Jul 1996 02:23:25 -0700 (PDT)" '
  435. '"IMAP4rev1 WG mtg summary and minutes" '
  436. '(("Terry Gray" NIL gray cac.washington.edu)) '
  437. '(("Terry Gray" NIL gray cac.washington.edu)) '
  438. '(("Terry Gray" NIL gray cac.washington.edu)) '
  439. '((NIL NIL imap cac.washington.edu)) '
  440. '((NIL NIL minutes CNRI.Reston.VA.US) '
  441. '("John Klensin" NIL KLENSIN INFOODS.MIT.EDU)) NIL NIL '
  442. '<B27397-0100000@cac.washington.edu>) '
  443. 'BODY (TEXT PLAIN (CHARSET US-ASCII) NIL NIL 7BIT 3028 92))',
  444. '("oo \\"oo\\" oo")',
  445. '("oo \\\\ oo")',
  446. '("oo \\ oo")',
  447. '("oo \\o")',
  448. '("oo \o")',
  449. '(oo \o)',
  450. '(oo \\o)',
  451. ]
  452. answers = [
  453. ['BODY.PEEK', ['HEADER.FIELDS.NOT', ['subject', 'bcc', 'cc']], s],
  454. ['FLAGS', [r'\Seen'], 'INTERNALDATE',
  455. '17-Jul-1996 02:44:25 -0700', 'RFC822.SIZE', '4286', 'ENVELOPE',
  456. ['Wed, 17 Jul 1996 02:23:25 -0700 (PDT)',
  457. 'IMAP4rev1 WG mtg summary and minutes', [["Terry Gray", None,
  458. "gray", "cac.washington.edu"]], [["Terry Gray", None,
  459. "gray", "cac.washington.edu"]], [["Terry Gray", None,
  460. "gray", "cac.washington.edu"]], [[None, None, "imap",
  461. "cac.washington.edu"]], [[None, None, "minutes",
  462. "CNRI.Reston.VA.US"], ["John Klensin", None, "KLENSIN",
  463. "INFOODS.MIT.EDU"]], None, None,
  464. "<B27397-0100000@cac.washington.edu>"], "BODY", ["TEXT", "PLAIN",
  465. ["CHARSET", "US-ASCII"], None, None, "7BIT", "3028", "92"]],
  466. ['oo "oo" oo'],
  467. ['oo \\\\ oo'],
  468. ['oo \\ oo'],
  469. ['oo \\o'],
  470. ['oo \o'],
  471. ['oo', '\o'],
  472. ['oo', '\\o'],
  473. ]
  474. for (case, expected) in zip(cases, answers):
  475. self.assertEqual(imap4.parseNestedParens(case), [expected])
  476. # XXX This code used to work, but changes occurred within the
  477. # imap4.py module which made it no longer necessary for *all* of it
  478. # to work. In particular, only the part that makes
  479. # 'BODY.PEEK[HEADER.FIELDS.NOT (Subject Bcc Cc)]' come out correctly
  480. # no longer needs to work. So, I am loathe to delete the entire
  481. # section of the test. --exarkun
  482. #
  483. # for (case, expected) in zip(answers, cases):
  484. # self.assertEqual('(' + imap4.collapseNestedLists(case) + ')', expected)
  485. def test_fetchParserSimple(self):
  486. cases = [
  487. ['ENVELOPE', 'Envelope'],
  488. ['FLAGS', 'Flags'],
  489. ['INTERNALDATE', 'InternalDate'],
  490. ['RFC822.HEADER', 'RFC822Header'],
  491. ['RFC822.SIZE', 'RFC822Size'],
  492. ['RFC822.TEXT', 'RFC822Text'],
  493. ['RFC822', 'RFC822'],
  494. ['UID', 'UID'],
  495. ['BODYSTRUCTURE', 'BodyStructure'],
  496. ]
  497. for (inp, outp) in cases:
  498. p = imap4._FetchParser()
  499. p.parseString(inp)
  500. self.assertEqual(len(p.result), 1)
  501. self.assertTrue(isinstance(p.result[0], getattr(p, outp)))
  502. def test_fetchParserMacros(self):
  503. cases = [
  504. ['ALL', (4, ['flags', 'internaldate', 'rfc822.size', 'envelope'])],
  505. ['FULL', (5, ['flags', 'internaldate', 'rfc822.size', 'envelope', 'body'])],
  506. ['FAST', (3, ['flags', 'internaldate', 'rfc822.size'])],
  507. ]
  508. for (inp, outp) in cases:
  509. p = imap4._FetchParser()
  510. p.parseString(inp)
  511. self.assertEqual(len(p.result), outp[0])
  512. expectedResult = [str(token).lower() for token in p.result]
  513. expectedResult.sort()
  514. outp[1].sort()
  515. self.assertEqual(expectedResult, outp[1])
  516. def test_fetchParserBody(self):
  517. P = imap4._FetchParser
  518. p = P()
  519. p.parseString('BODY')
  520. self.assertEqual(len(p.result), 1)
  521. self.assertTrue(isinstance(p.result[0], p.Body))
  522. self.assertEqual(p.result[0].peek, False)
  523. self.assertEqual(p.result[0].header, None)
  524. self.assertEqual(str(p.result[0]), 'BODY')
  525. p = P()
  526. p.parseString('BODY.PEEK')
  527. self.assertEqual(len(p.result), 1)
  528. self.assertTrue(isinstance(p.result[0], p.Body))
  529. self.assertEqual(p.result[0].peek, True)
  530. self.assertEqual(str(p.result[0]), 'BODY')
  531. p = P()
  532. p.parseString('BODY[]')
  533. self.assertEqual(len(p.result), 1)
  534. self.assertTrue(isinstance(p.result[0], p.Body))
  535. self.assertEqual(p.result[0].empty, True)
  536. self.assertEqual(str(p.result[0]), 'BODY[]')
  537. p = P()
  538. p.parseString('BODY[HEADER]')
  539. self.assertEqual(len(p.result), 1)
  540. self.assertTrue(isinstance(p.result[0], p.Body))
  541. self.assertEqual(p.result[0].peek, False)
  542. self.assertTrue(isinstance(p.result[0].header, p.Header))
  543. self.assertEqual(p.result[0].header.negate, True)
  544. self.assertEqual(p.result[0].header.fields, ())
  545. self.assertEqual(p.result[0].empty, False)
  546. self.assertEqual(str(p.result[0]), 'BODY[HEADER]')
  547. p = P()
  548. p.parseString('BODY.PEEK[HEADER]')
  549. self.assertEqual(len(p.result), 1)
  550. self.assertTrue(isinstance(p.result[0], p.Body))
  551. self.assertEqual(p.result[0].peek, True)
  552. self.assertTrue(isinstance(p.result[0].header, p.Header))
  553. self.assertEqual(p.result[0].header.negate, True)
  554. self.assertEqual(p.result[0].header.fields, ())
  555. self.assertEqual(p.result[0].empty, False)
  556. self.assertEqual(str(p.result[0]), 'BODY[HEADER]')
  557. p = P()
  558. p.parseString('BODY[HEADER.FIELDS (Subject Cc Message-Id)]')
  559. self.assertEqual(len(p.result), 1)
  560. self.assertTrue(isinstance(p.result[0], p.Body))
  561. self.assertEqual(p.result[0].peek, False)
  562. self.assertTrue(isinstance(p.result[0].header, p.Header))
  563. self.assertEqual(p.result[0].header.negate, False)
  564. self.assertEqual(p.result[0].header.fields, ['SUBJECT', 'CC', 'MESSAGE-ID'])
  565. self.assertEqual(p.result[0].empty, False)
  566. self.assertEqual(str(p.result[0]), 'BODY[HEADER.FIELDS (Subject Cc Message-Id)]')
  567. p = P()
  568. p.parseString('BODY.PEEK[HEADER.FIELDS (Subject Cc Message-Id)]')
  569. self.assertEqual(len(p.result), 1)
  570. self.assertTrue(isinstance(p.result[0], p.Body))
  571. self.assertEqual(p.result[0].peek, True)
  572. self.assertTrue(isinstance(p.result[0].header, p.Header))
  573. self.assertEqual(p.result[0].header.negate, False)
  574. self.assertEqual(p.result[0].header.fields, ['SUBJECT', 'CC', 'MESSAGE-ID'])
  575. self.assertEqual(p.result[0].empty, False)
  576. self.assertEqual(str(p.result[0]), 'BODY[HEADER.FIELDS (Subject Cc Message-Id)]')
  577. p = P()
  578. p.parseString('BODY.PEEK[HEADER.FIELDS.NOT (Subject Cc Message-Id)]')
  579. self.assertEqual(len(p.result), 1)
  580. self.assertTrue(isinstance(p.result[0], p.Body))
  581. self.assertEqual(p.result[0].peek, True)
  582. self.assertTrue(isinstance(p.result[0].header, p.Header))
  583. self.assertEqual(p.result[0].header.negate, True)
  584. self.assertEqual(p.result[0].header.fields, ['SUBJECT', 'CC', 'MESSAGE-ID'])
  585. self.assertEqual(p.result[0].empty, False)
  586. self.assertEqual(str(p.result[0]), 'BODY[HEADER.FIELDS.NOT (Subject Cc Message-Id)]')
  587. p = P()
  588. p.parseString('BODY[1.MIME]<10.50>')
  589. self.assertEqual(len(p.result), 1)
  590. self.assertTrue(isinstance(p.result[0], p.Body))
  591. self.assertEqual(p.result[0].peek, False)
  592. self.assertTrue(isinstance(p.result[0].mime, p.MIME))
  593. self.assertEqual(p.result[0].part, (0,))
  594. self.assertEqual(p.result[0].partialBegin, 10)
  595. self.assertEqual(p.result[0].partialLength, 50)
  596. self.assertEqual(p.result[0].empty, False)
  597. self.assertEqual(str(p.result[0]), 'BODY[1.MIME]<10.50>')
  598. p = P()
  599. p.parseString('BODY.PEEK[1.3.9.11.HEADER.FIELDS.NOT (Message-Id Date)]<103.69>')
  600. self.assertEqual(len(p.result), 1)
  601. self.assertTrue(isinstance(p.result[0], p.Body))
  602. self.assertEqual(p.result[0].peek, True)
  603. self.assertTrue(isinstance(p.result[0].header, p.Header))
  604. self.assertEqual(p.result[0].part, (0, 2, 8, 10))
  605. self.assertEqual(p.result[0].header.fields, ['MESSAGE-ID', 'DATE'])
  606. self.assertEqual(p.result[0].partialBegin, 103)
  607. self.assertEqual(p.result[0].partialLength, 69)
  608. self.assertEqual(p.result[0].empty, False)
  609. self.assertEqual(str(p.result[0]), 'BODY[1.3.9.11.HEADER.FIELDS.NOT (Message-Id Date)]<103.69>')
  610. def test_files(self):
  611. inputStructure = [
  612. 'foo', 'bar', 'baz', BytesIO(b'this is a file\r\n'), 'buz',
  613. u'biz'
  614. ]
  615. output = '"foo" "bar" "baz" {16}\r\nthis is a file\r\n "buz" "biz"'
  616. self.assertEqual(imap4.collapseNestedLists(inputStructure), output)
  617. def test_quoteAvoider(self):
  618. input = [
  619. 'foo', imap4.DontQuoteMe('bar'), "baz", BytesIO(b'this is a file\r\n'),
  620. imap4.DontQuoteMe('buz'), ""
  621. ]
  622. output = '"foo" bar "baz" {16}\r\nthis is a file\r\n buz ""'
  623. self.assertEqual(imap4.collapseNestedLists(input), output)
  624. def test_literals(self):
  625. cases = [
  626. ('({10}\r\n0123456789)', [['0123456789']]),
  627. ]
  628. for (case, expected) in cases:
  629. self.assertEqual(imap4.parseNestedParens(case), expected)
  630. def test_queryBuilder(self):
  631. inputs = [
  632. imap4.Query(flagged=1),
  633. imap4.Query(sorted=1, unflagged=1, deleted=1),
  634. imap4.Or(imap4.Query(flagged=1), imap4.Query(deleted=1)),
  635. imap4.Query(before='today'),
  636. imap4.Or(
  637. imap4.Query(deleted=1),
  638. imap4.Query(unseen=1),
  639. imap4.Query(new=1)
  640. ),
  641. imap4.Or(
  642. imap4.Not(
  643. imap4.Or(
  644. imap4.Query(sorted=1, since='yesterday', smaller=1000),
  645. imap4.Query(sorted=1, before='tuesday', larger=10000),
  646. imap4.Query(sorted=1, unseen=1, deleted=1, before='today'),
  647. imap4.Not(
  648. imap4.Query(subject='spam')
  649. ),
  650. ),
  651. ),
  652. imap4.Not(
  653. imap4.Query(uid='1:5')
  654. ),
  655. )
  656. ]
  657. outputs = [
  658. 'FLAGGED',
  659. '(DELETED UNFLAGGED)',
  660. '(OR FLAGGED DELETED)',
  661. '(BEFORE "today")',
  662. '(OR DELETED (OR UNSEEN NEW))',
  663. '(OR (NOT (OR (SINCE "yesterday" SMALLER 1000) ' # Continuing
  664. '(OR (BEFORE "tuesday" LARGER 10000) (OR (BEFORE ' # Some more
  665. '"today" DELETED UNSEEN) (NOT (SUBJECT "spam")))))) ' # And more
  666. '(NOT (UID 1:5)))',
  667. ]
  668. for (query, expected) in zip(inputs, outputs):
  669. self.assertEqual(query, expected)
  670. def test_queryKeywordFlagWithQuotes(self):
  671. """
  672. When passed the C{keyword} argument, L{imap4.Query} returns an unquoted
  673. string.
  674. @see: U{http://tools.ietf.org/html/rfc3501#section-9}
  675. @see: U{http://tools.ietf.org/html/rfc3501#section-6.4.4}
  676. """
  677. query = imap4.Query(keyword='twisted')
  678. self.assertEqual('(KEYWORD twisted)', query)
  679. def test_queryUnkeywordFlagWithQuotes(self):
  680. """
  681. When passed the C{unkeyword} argument, L{imap4.Query} returns an
  682. unquoted string.
  683. @see: U{http://tools.ietf.org/html/rfc3501#section-9}
  684. @see: U{http://tools.ietf.org/html/rfc3501#section-6.4.4}
  685. """
  686. query = imap4.Query(unkeyword='twisted')
  687. self.assertEqual('(UNKEYWORD twisted)', query)
  688. def _keywordFilteringTest(self, keyword):
  689. """
  690. Helper to implement tests for value filtering of KEYWORD and UNKEYWORD
  691. queries.
  692. @param keyword: A native string giving the name of the L{imap4.Query}
  693. keyword argument to test.
  694. """
  695. # Check all the printable exclusions
  696. self.assertEqual(
  697. '(%s twistedrocks)' % (keyword.upper(),),
  698. imap4.Query(**{keyword: r'twisted (){%*"\] rocks'}))
  699. # Check all the non-printable exclusions
  700. self.assertEqual(
  701. '(%s twistedrocks)' % (keyword.upper(),),
  702. imap4.Query(**{
  703. keyword: 'twisted %s rocks' % (
  704. ''.join(chr(ch) for ch in range(33)),)}))
  705. def test_queryKeywordFlag(self):
  706. """
  707. When passed the C{keyword} argument, L{imap4.Query} returns an
  708. C{atom} that consists of one or more non-special characters.
  709. List of the invalid characters:
  710. ( ) { % * " \ ] CTL SP
  711. @see: U{ABNF definition of CTL and SP<https://tools.ietf.org/html/rfc2234>}
  712. @see: U{IMAP4 grammar<http://tools.ietf.org/html/rfc3501#section-9>}
  713. @see: U{IMAP4 SEARCH specification<http://tools.ietf.org/html/rfc3501#section-6.4.4>}
  714. """
  715. self._keywordFilteringTest("keyword")
  716. def test_queryUnkeywordFlag(self):
  717. """
  718. When passed the C{unkeyword} argument, L{imap4.Query} returns an
  719. C{atom} that consists of one or more non-special characters.
  720. List of the invalid characters:
  721. ( ) { % * " \ ] CTL SP
  722. @see: U{ABNF definition of CTL and SP<https://tools.ietf.org/html/rfc2234>}
  723. @see: U{IMAP4 grammar<http://tools.ietf.org/html/rfc3501#section-9>}
  724. @see: U{IMAP4 SEARCH specification<http://tools.ietf.org/html/rfc3501#section-6.4.4>}
  725. """
  726. self._keywordFilteringTest("unkeyword")
  727. def test_invalidIdListParser(self):
  728. """
  729. Trying to parse an invalid representation of a sequence range raises an
  730. L{IllegalIdentifierError}.
  731. """
  732. inputs = [
  733. '*:*',
  734. 'foo',
  735. '4:',
  736. 'bar:5'
  737. ]
  738. for input in inputs:
  739. self.assertRaises(imap4.IllegalIdentifierError,
  740. imap4.parseIdList, input, 12345)
  741. def test_invalidIdListParserNonPositive(self):
  742. """
  743. Zeroes and negative values are not accepted in id range expressions. RFC
  744. 3501 states that sequence numbers and sequence ranges consist of
  745. non-negative numbers (RFC 3501 section 9, the seq-number grammar item).
  746. """
  747. inputs = [
  748. '0:5',
  749. '0:0',
  750. '*:0',
  751. '0',
  752. '-3:5',
  753. '1:-2',
  754. '-1'
  755. ]
  756. for input in inputs:
  757. self.assertRaises(imap4.IllegalIdentifierError,
  758. imap4.parseIdList, input, 12345)
  759. def test_parseIdList(self):
  760. """
  761. The function to parse sequence ranges yields appropriate L{MessageSet}
  762. objects.
  763. """
  764. inputs = [
  765. '1:*',
  766. '5:*',
  767. '1:2,5:*',
  768. '*',
  769. '1',
  770. '1,2',
  771. '1,3,5',
  772. '1:10',
  773. '1:10,11',
  774. '1:5,10:20',
  775. '1,5:10',
  776. '1,5:10,15:20',
  777. '1:10,15,20:25',
  778. '4:2'
  779. ]
  780. outputs = [
  781. MessageSet(1, None),
  782. MessageSet(5, None),
  783. MessageSet(5, None) + MessageSet(1, 2),
  784. MessageSet(None, None),
  785. MessageSet(1),
  786. MessageSet(1, 2),
  787. MessageSet(1) + MessageSet(3) + MessageSet(5),
  788. MessageSet(1, 10),
  789. MessageSet(1, 11),
  790. MessageSet(1, 5) + MessageSet(10, 20),
  791. MessageSet(1) + MessageSet(5, 10),
  792. MessageSet(1) + MessageSet(5, 10) + MessageSet(15, 20),
  793. MessageSet(1, 10) + MessageSet(15) + MessageSet(20, 25),
  794. MessageSet(2, 4),
  795. ]
  796. lengths = [
  797. None, None, None,
  798. 1, 1, 2, 3, 10, 11, 16, 7, 13, 17, 3
  799. ]
  800. for (input, expected) in zip(inputs, outputs):
  801. self.assertEqual(imap4.parseIdList(input), expected)
  802. for (input, expected) in zip(inputs, lengths):
  803. if expected is None:
  804. self.assertRaises(TypeError, len, imap4.parseIdList(input))
  805. else:
  806. L = len(imap4.parseIdList(input))
  807. self.assertEqual(L, expected,
  808. "len(%r) = %r != %r" % (input, L, expected))
  809. @implementer(imap4.IMailboxInfo, imap4.IMailbox, imap4.ICloseableMailbox)
  810. class SimpleMailbox:
  811. flags = ('\\Flag1', 'Flag2', '\\AnotherSysFlag', 'LastFlag')
  812. messages = []
  813. mUID = 0
  814. rw = 1
  815. closed = False
  816. def __init__(self):
  817. self.listeners = []
  818. self.addListener = self.listeners.append
  819. self.removeListener = self.listeners.remove
  820. def getFlags(self):
  821. return self.flags
  822. def getUIDValidity(self):
  823. return 42
  824. def getUIDNext(self):
  825. return len(self.messages) + 1
  826. def getMessageCount(self):
  827. return 9
  828. def getRecentCount(self):
  829. return 3
  830. def getUnseenCount(self):
  831. return 4
  832. def isWriteable(self):
  833. return self.rw
  834. def destroy(self):
  835. pass
  836. def getHierarchicalDelimiter(self):
  837. return '/'
  838. def requestStatus(self, names):
  839. r = {}
  840. if 'MESSAGES' in names:
  841. r['MESSAGES'] = self.getMessageCount()
  842. if 'RECENT' in names:
  843. r['RECENT'] = self.getRecentCount()
  844. if 'UIDNEXT' in names:
  845. r['UIDNEXT'] = self.getMessageCount() + 1
  846. if 'UIDVALIDITY' in names:
  847. r['UIDVALIDITY'] = self.getUID()
  848. if 'UNSEEN' in names:
  849. r['UNSEEN'] = self.getUnseenCount()
  850. return defer.succeed(r)
  851. def addMessage(self, message, flags, date = None):
  852. self.messages.append((message, flags, date, self.mUID))
  853. self.mUID += 1
  854. return defer.succeed(None)
  855. def expunge(self):
  856. delete = []
  857. for i in self.messages:
  858. if '\\Deleted' in i[1]:
  859. delete.append(i)
  860. for i in delete:
  861. self.messages.remove(i)
  862. return [i[3] for i in delete]
  863. def close(self):
  864. self.closed = True
  865. class Account(imap4.MemoryAccount):
  866. mailboxFactory = SimpleMailbox
  867. def _emptyMailbox(self, name, id):
  868. return self.mailboxFactory()
  869. def select(self, name, rw=1):
  870. mbox = imap4.MemoryAccount.select(self, name)
  871. if mbox is not None:
  872. mbox.rw = rw
  873. return mbox
  874. class SimpleServer(imap4.IMAP4Server):
  875. def __init__(self, *args, **kw):
  876. imap4.IMAP4Server.__init__(self, *args, **kw)
  877. realm = TestRealm()
  878. realm.theAccount = Account(b'testuser')
  879. portal = Portal(realm)
  880. c = InMemoryUsernamePasswordDatabaseDontUse()
  881. self.checker = c
  882. self.portal = portal
  883. portal.registerChecker(c)
  884. self.timeoutTest = False
  885. def lineReceived(self, line):
  886. if self.timeoutTest:
  887. #Do not send a response
  888. return
  889. imap4.IMAP4Server.lineReceived(self, line)
  890. _username = b'testuser'
  891. _password = b'password-test'
  892. def authenticateLogin(self, username, password):
  893. if username == self._username and password == self._password:
  894. return imap4.IAccount, self.theAccount, lambda: None
  895. raise UnauthorizedLogin()
  896. class SimpleClient(imap4.IMAP4Client):
  897. def __init__(self, deferred, contextFactory = None):
  898. imap4.IMAP4Client.__init__(self, contextFactory)
  899. self.deferred = deferred
  900. self.events = []
  901. def serverGreeting(self, caps):
  902. self.deferred.callback(None)
  903. def modeChanged(self, writeable):
  904. self.events.append([b'modeChanged', writeable])
  905. self.transport.loseConnection()
  906. def flagsChanged(self, newFlags):
  907. self.events.append([b'flagsChanged', newFlags])
  908. self.transport.loseConnection()
  909. def newMessages(self, exists, recent):
  910. self.events.append([b'newMessages', exists, recent])
  911. self.transport.loseConnection()
  912. class IMAP4HelperMixin:
  913. serverCTX = None
  914. clientCTX = None
  915. def setUp(self):
  916. d = defer.Deferred()
  917. self.server = SimpleServer(contextFactory=self.serverCTX)
  918. self.client = SimpleClient(d, contextFactory=self.clientCTX)
  919. self.connected = d
  920. SimpleMailbox.messages = []
  921. theAccount = Account(b'testuser')
  922. theAccount.mboxType = SimpleMailbox
  923. SimpleServer.theAccount = theAccount
  924. def tearDown(self):
  925. del self.server
  926. del self.client
  927. del self.connected
  928. def _cbStopClient(self, ignore):
  929. self.client.transport.loseConnection()
  930. def _ebGeneral(self, failure):
  931. self.client.transport.loseConnection()
  932. self.server.transport.loseConnection()
  933. log.err(failure, "Problem with " + str(self))
  934. def loopback(self):
  935. return loopback.loopbackAsync(self.server, self.client)
  936. class IMAP4ServerTests(IMAP4HelperMixin, unittest.TestCase):
  937. def testCapability(self):
  938. caps = {}
  939. def getCaps():
  940. def gotCaps(c):
  941. caps.update(c)
  942. self.server.transport.loseConnection()
  943. return self.client.getCapabilities().addCallback(gotCaps)
  944. d1 = self.connected.addCallback(strip(getCaps)).addErrback(self._ebGeneral)
  945. d = defer.gatherResults([self.loopback(), d1])
  946. expected = {b'IMAP4rev1': None, b'NAMESPACE': None, b'IDLE': None}
  947. return d.addCallback(lambda _: self.assertEqual(expected, caps))
  948. def testCapabilityWithAuth(self):
  949. caps = {}
  950. self.server.challengers[b'CRAM-MD5'] = CramMD5Credentials
  951. def getCaps():
  952. def gotCaps(c):
  953. caps.update(c)
  954. self.server.transport.loseConnection()
  955. return self.client.getCapabilities().addCallback(gotCaps)
  956. d1 = self.connected.addCallback(strip(getCaps)).addErrback(self._ebGeneral)
  957. d = defer.gatherResults([self.loopback(), d1])
  958. expCap = {b'IMAP4rev1': None, b'NAMESPACE': None,
  959. b'IDLE': None, b'AUTH': [b'CRAM-MD5']}
  960. return d.addCallback(lambda _: self.assertEqual(expCap, caps))
  961. def testLogout(self):
  962. self.loggedOut = 0
  963. def logout():
  964. def setLoggedOut():
  965. self.loggedOut = 1
  966. self.client.logout().addCallback(strip(setLoggedOut))
  967. self.connected.addCallback(strip(logout)).addErrback(self._ebGeneral)
  968. d = self.loopback()
  969. return d.addCallback(lambda _: self.assertEqual(self.loggedOut, 1))
  970. def testNoop(self):
  971. self.responses = None
  972. def noop():
  973. def setResponses(responses):
  974. self.responses = responses
  975. self.server.transport.loseConnection()
  976. self.client.noop().addCallback(setResponses)
  977. self.connected.addCallback(strip(noop)).addErrback(self._ebGeneral)
  978. d = self.loopback()
  979. return d.addCallback(lambda _: self.assertEqual(self.responses, []))
  980. def testLogin(self):
  981. def login():
  982. d = self.client.login(b'testuser', b'password-test')
  983. d.addCallback(self._cbStopClient)
  984. d1 = self.connected.addCallback(strip(login)).addErrback(self._ebGeneral)
  985. d = defer.gatherResults([d1, self.loopback()])
  986. return d.addCallback(self._cbTestLogin)
  987. def _cbTestLogin(self, ignored):
  988. self.assertEqual(self.server.account, SimpleServer.theAccount)
  989. self.assertEqual(self.server.state, 'auth')
  990. def testFailedLogin(self):
  991. def login():
  992. d = self.client.login(b'testuser', b'wrong-password')
  993. d.addBoth(self._cbStopClient)
  994. d1 = self.connected.addCallback(strip(login)).addErrback(self._ebGeneral)
  995. d2 = self.loopback()
  996. d = defer.gatherResults([d1, d2])
  997. return d.addCallback(self._cbTestFailedLogin)
  998. def _cbTestFailedLogin(self, ignored):
  999. self.assertEqual(self.server.account, None)
  1000. self.assertEqual(self.server.state, 'unauth')
  1001. def testLoginRequiringQuoting(self):
  1002. self.server._username = '{test}user'
  1003. self.server._password = '{test}password'
  1004. def login():
  1005. d = self.client.login('{test}user', '{test}password')
  1006. d.addBoth(self._cbStopClient)
  1007. d1 = self.connected.addCallback(strip(login)).addErrback(self._ebGeneral)
  1008. d = defer.gatherResults([self.loopback(), d1])
  1009. return d.addCallback(self._cbTestLoginRequiringQuoting)
  1010. def _cbTestLoginRequiringQuoting(self, ignored):
  1011. self.assertEqual(self.server.account, SimpleServer.theAccount)
  1012. self.assertEqual(self.server.state, 'auth')
  1013. def testNamespace(self):
  1014. self.namespaceArgs = None
  1015. def login():
  1016. return self.client.login(b'testuser', b'password-test')
  1017. def namespace():
  1018. def gotNamespace(args):
  1019. self.namespaceArgs = args
  1020. self._cbStopClient(None)
  1021. return self.client.namespace().addCallback(gotNamespace)
  1022. d1 = self.connected.addCallback(strip(login))
  1023. d1.addCallback(strip(namespace))
  1024. d1.addErrback(self._ebGeneral)
  1025. d2 = self.loopback()
  1026. d = defer.gatherResults([d1, d2])
  1027. d.addCallback(lambda _: self.assertEqual(self.namespaceArgs,
  1028. [[['', '/']], [], []]))
  1029. return d
  1030. def testSelect(self):
  1031. SimpleServer.theAccount.addMailbox('test-mailbox')
  1032. self.selectedArgs = None
  1033. def login():
  1034. return self.client.login(b'testuser', b'password-test')
  1035. def select():
  1036. def selected(args):
  1037. self.selectedArgs = args
  1038. self._cbStopClient(None)
  1039. d = self.client.select('test-mailbox')
  1040. d.addCallback(selected)
  1041. return d
  1042. d1 = self.connected.addCallback(strip(login))
  1043. d1.addCallback(strip(select))
  1044. d1.addErrback(self._ebGeneral)
  1045. d2 = self.loopback()
  1046. return defer.gatherResults([d1, d2]).addCallback(self._cbTestSelect)
  1047. def _cbTestSelect(self, ignored):
  1048. mbox = SimpleServer.theAccount.mailboxes['TEST-MAILBOX']
  1049. self.assertEqual(self.server.mbox, mbox)
  1050. self.assertEqual(self.selectedArgs, {
  1051. 'EXISTS': 9, 'RECENT': 3, 'UIDVALIDITY': 42,
  1052. 'FLAGS': ('\\Flag1', 'Flag2', '\\AnotherSysFlag', 'LastFlag'),
  1053. 'READ-WRITE': 1
  1054. })
  1055. def test_examine(self):
  1056. """
  1057. L{IMAP4Client.examine} issues an I{EXAMINE} command to the server and
  1058. returns a L{Deferred} which fires with a C{dict} with as many of the
  1059. following keys as the server includes in its response: C{'FLAGS'},
  1060. C{'EXISTS'}, C{'RECENT'}, C{'UNSEEN'}, C{'READ-WRITE'}, C{'READ-ONLY'},
  1061. C{'UIDVALIDITY'}, and C{'PERMANENTFLAGS'}.
  1062. Unfortunately the server doesn't generate all of these so it's hard to
  1063. test the client's handling of them here. See
  1064. L{IMAP4ClientExamineTests} below.
  1065. See U{RFC 3501<http://www.faqs.org/rfcs/rfc3501.html>}, section 6.3.2,
  1066. for details.
  1067. """
  1068. SimpleServer.theAccount.addMailbox('test-mailbox')
  1069. self.examinedArgs = None
  1070. def login():
  1071. return self.client.login(b'testuser', b'password-test')
  1072. def examine():
  1073. def examined(args):
  1074. self.examinedArgs = args
  1075. self._cbStopClient(None)
  1076. d = self.client.examine('test-mailbox')
  1077. d.addCallback(examined)
  1078. return d
  1079. d1 = self.connected.addCallback(strip(login))
  1080. d1.addCallback(strip(examine))
  1081. d1.addErrback(self._ebGeneral)
  1082. d2 = self.loopback()
  1083. d = defer.gatherResults([d1, d2])
  1084. return d.addCallback(self._cbTestExamine)
  1085. def _cbTestExamine(self, ignored):
  1086. mbox = SimpleServer.theAccount.mailboxes['TEST-MAILBOX']
  1087. self.assertEqual(self.server.mbox, mbox)
  1088. self.assertEqual(self.examinedArgs, {
  1089. 'EXISTS': 9, 'RECENT': 3, 'UIDVALIDITY': 42,
  1090. 'FLAGS': ('\\Flag1', 'Flag2', '\\AnotherSysFlag', 'LastFlag'),
  1091. 'READ-WRITE': False})
  1092. def testCreate(self):
  1093. succeed = ('testbox', 'test/box', 'test/', 'test/box/box', 'INBOX')
  1094. fail = ('testbox', 'test/box')
  1095. def cb(): self.result.append(1)
  1096. def eb(failure): self.result.append(0)
  1097. def login():
  1098. return self.client.login(b'testuser', b'password-test')
  1099. def create():
  1100. for name in succeed + fail:
  1101. d = self.client.create(name)
  1102. d.addCallback(strip(cb)).addErrback(eb)
  1103. d.addCallbacks(self._cbStopClient, self._ebGeneral)
  1104. self.result = []
  1105. d1 = self.connected.addCallback(strip(login)).addCallback(strip(create))
  1106. d2 = self.loopback()
  1107. d = defer.gatherResults([d1, d2])
  1108. return d.addCallback(self._cbTestCreate, succeed, fail)
  1109. def _cbTestCreate(self, ignored, succeed, fail):
  1110. self.assertEqual(self.result, [1] * len(succeed) + [0] * len(fail))
  1111. mbox = SimpleServer.theAccount.mailboxes.keys()
  1112. answers = ['inbox', 'testbox', 'test/box', 'test', 'test/box/box']
  1113. mbox.sort()
  1114. answers.sort()
  1115. self.assertEqual(mbox, [a.upper() for a in answers])
  1116. def testDelete(self):
  1117. SimpleServer.theAccount.addMailbox('delete/me')
  1118. def login():
  1119. return self.client.login(b'testuser', b'password-test')
  1120. def delete():
  1121. return self.client.delete('delete/me')
  1122. d1 = self.connected.addCallback(strip(login))
  1123. d1.addCallbacks(strip(delete), self._ebGeneral)
  1124. d1.addCallbacks(self._cbStopClient, self._ebGeneral)
  1125. d2 = self.loopback()
  1126. d = defer.gatherResults([d1, d2])
  1127. d.addCallback(lambda _:
  1128. self.assertEqual(SimpleServer.theAccount.mailboxes.keys(), []))
  1129. return d
  1130. def testIllegalInboxDelete(self):
  1131. self.stashed = None
  1132. def login():
  1133. return self.client.login(b'testuser', b'password-test')
  1134. def delete():
  1135. return self.client.delete('inbox')
  1136. def stash(result):
  1137. self.stashed = result
  1138. d1 = self.connected.addCallback(strip(login))
  1139. d1.addCallbacks(strip(delete), self._ebGeneral)
  1140. d1.addBoth(stash)
  1141. d1.addCallbacks(self._cbStopClient, self._ebGeneral)
  1142. d2 = self.loopback()
  1143. d = defer.gatherResults([d1, d2])
  1144. d.addCallback(lambda _: self.assertTrue(isinstance(self.stashed,
  1145. failure.Failure)))
  1146. return d
  1147. def testNonExistentDelete(self):
  1148. def login():
  1149. return self.client.login(b'testuser', b'password-test')
  1150. def delete():
  1151. return self.client.delete('delete/me')
  1152. def deleteFailed(failure):
  1153. self.failure = failure
  1154. self.failure = None
  1155. d1 = self.connected.addCallback(strip(login))
  1156. d1.addCallback(strip(delete)).addErrback(deleteFailed)
  1157. d1.addCallbacks(self._cbStopClient, self._ebGeneral)
  1158. d2 = self.loopback()
  1159. d = defer.gatherResults([d1, d2])
  1160. d.addCallback(lambda _: self.assertEqual(str(self.failure.value),
  1161. 'No such mailbox'))
  1162. return d
  1163. def testIllegalDelete(self):
  1164. m = SimpleMailbox()
  1165. m.flags = (r'\Noselect',)
  1166. SimpleServer.theAccount.addMailbox('delete', m)
  1167. SimpleServer.theAccount.addMailbox('delete/me')
  1168. def login():
  1169. return self.client.login(b'testuser', b'password-test')
  1170. def delete():
  1171. return self.client.delete('delete')
  1172. def deleteFailed(failure):
  1173. self.failure = failure
  1174. self.failure = None
  1175. d1 = self.connected.addCallback(strip(login))
  1176. d1.addCallback(strip(delete)).addErrback(deleteFailed)
  1177. d1.addCallbacks(self._cbStopClient, self._ebGeneral)
  1178. d2 = self.loopback()
  1179. d = defer.gatherResults([d1, d2])
  1180. expected = "Hierarchically inferior mailboxes exist and \\Noselect is set"
  1181. d.addCallback(lambda _:
  1182. self.assertEqual(str(self.failure.value), expected))
  1183. return d
  1184. def testRename(self):
  1185. SimpleServer.theAccount.addMailbox(b'oldmbox')
  1186. def login():
  1187. return self.client.login(b'testuser', b'password-test')
  1188. def rename():
  1189. return self.client.rename(b'oldmbox', b'newname')
  1190. d1 = self.connected.addCallback(strip(login))
  1191. d1.addCallbacks(strip(rename), self._ebGeneral)
  1192. d1.addCallbacks(self._cbStopClient, self._ebGeneral)
  1193. d2 = self.loopback()
  1194. d = defer.gatherResults([d1, d2])
  1195. d.addCallback(lambda _:
  1196. self.assertEqual(SimpleServer.theAccount.mailboxes.keys(),
  1197. [b'NEWNAME']))
  1198. return d
  1199. def testIllegalInboxRename(self):
  1200. self.stashed = None
  1201. def login():
  1202. return self.client.login(b'testuser', b'password-test')
  1203. def rename():
  1204. return self.client.rename('inbox', 'frotz')
  1205. def stash(stuff):
  1206. self.stashed = stuff
  1207. d1 = self.connected.addCallback(strip(login))
  1208. d1.addCallbacks(strip(rename), self._ebGeneral)
  1209. d1.addBoth(stash)
  1210. d1.addCallbacks(self._cbStopClient, self._ebGeneral)
  1211. d2 = self.loopback()
  1212. d = defer.gatherResults([d1, d2])
  1213. d.addCallback(lambda _:
  1214. self.assertTrue(isinstance(self.stashed, failure.Failure)))
  1215. return d
  1216. def testHierarchicalRename(self):
  1217. SimpleServer.theAccount.create(b'oldmbox/m1')
  1218. SimpleServer.theAccount.create(b'oldmbox/m2')
  1219. def login():
  1220. return self.client.login(b'testuser', b'password-test')
  1221. def rename():
  1222. return self.client.rename(b'oldmbox', b'newname')
  1223. d1 = self.connected.addCallback(strip(login))
  1224. d1.addCallbacks(strip(rename), self._ebGeneral)
  1225. d1.addCallbacks(self._cbStopClient, self._ebGeneral)
  1226. d2 = self.loopback()
  1227. d = defer.gatherResults([d1, d2])
  1228. return d.addCallback(self._cbTestHierarchicalRename)
  1229. def _cbTestHierarchicalRename(self, ignored):
  1230. mboxes = SimpleServer.theAccount.mailboxes.keys()
  1231. expected = [b'newname', b'newname/m1', b'newname/m2']
  1232. mboxes = list(sorted(mboxes))
  1233. self.assertEqual(mboxes, [s.upper() for s in expected])
  1234. def testSubscribe(self):
  1235. def login():
  1236. return self.client.login(b'testuser', b'password-test')
  1237. def subscribe():
  1238. return self.client.subscribe('this/mbox')
  1239. d1 = self.connected.addCallback(strip(login))
  1240. d1.addCallbacks(strip(subscribe), self._ebGeneral)
  1241. d1.addCallbacks(self._cbStopClient, self._ebGeneral)
  1242. d2 = self.loopback()
  1243. d = defer.gatherResults([d1, d2])
  1244. d.addCallback(lambda _:
  1245. self.assertEqual(SimpleServer.theAccount.subscriptions,
  1246. ['THIS/MBOX']))
  1247. return d
  1248. def testUnsubscribe(self):
  1249. SimpleServer.theAccount.subscriptions = ['THIS/MBOX', 'THAT/MBOX']
  1250. def login():
  1251. return self.client.login(b'testuser', b'password-test')
  1252. def unsubscribe():
  1253. return self.client.unsubscribe('this/mbox')
  1254. d1 = self.connected.addCallback(strip(login))
  1255. d1.addCallbacks(strip(unsubscribe), self._ebGeneral)
  1256. d1.addCallbacks(self._cbStopClient, self._ebGeneral)
  1257. d2 = self.loopback()
  1258. d = defer.gatherResults([d1, d2])
  1259. d.addCallback(lambda _:
  1260. self.assertEqual(SimpleServer.theAccount.subscriptions,
  1261. ['THAT/MBOX']))
  1262. return d
  1263. def _listSetup(self, f):
  1264. SimpleServer.theAccount.addMailbox('root/subthing')
  1265. SimpleServer.theAccount.addMailbox('root/another-thing')
  1266. SimpleServer.theAccount.addMailbox('non-root/subthing')
  1267. def login():
  1268. return self.client.login(b'testuser', b'password-test')
  1269. def listed(answers):
  1270. self.listed = answers
  1271. self.listed = None
  1272. d1 = self.connected.addCallback(strip(login))
  1273. d1.addCallbacks(strip(f), self._ebGeneral)
  1274. d1.addCallbacks(listed, self._ebGeneral)
  1275. d1.addCallbacks(self._cbStopClient, self._ebGeneral)
  1276. d2 = self.loopback()
  1277. return defer.gatherResults([d1, d2]).addCallback(lambda _: self.listed)
  1278. def testList(self):
  1279. def mailboxList():
  1280. return self.client.list('root', '%')
  1281. d = self._listSetup(mailboxList)
  1282. d.addCallback(lambda listed: self.assertEqual(
  1283. sortNest(listed),
  1284. sortNest([
  1285. (SimpleMailbox.flags, "/", "ROOT/SUBTHING"),
  1286. (SimpleMailbox.flags, "/", "ROOT/ANOTHER-THING")
  1287. ])
  1288. ))
  1289. return d
  1290. def testLSub(self):
  1291. SimpleServer.theAccount.subscribe('ROOT/SUBTHING')
  1292. def lsub():
  1293. return self.client.lsub('root', '%')
  1294. d = self._listSetup(lsub)
  1295. d.addCallback(self.assertEqual,
  1296. [(SimpleMailbox.flags, "/", "ROOT/SUBTHING")])
  1297. return d
  1298. def testStatus(self):
  1299. SimpleServer.theAccount.addMailbox('root/subthing')
  1300. def login():
  1301. return self.client.login(b'testuser', b'password-test')
  1302. def status():
  1303. return self.client.status('root/subthing', 'MESSAGES', 'UIDNEXT', 'UNSEEN')
  1304. def statused(result):
  1305. self.statused = result
  1306. self.statused = None
  1307. d1 = self.connected.addCallback(strip(login))
  1308. d1.addCallbacks(strip(status), self._ebGeneral)
  1309. d1.addCallbacks(statused, self._ebGeneral)
  1310. d1.addCallbacks(self._cbStopClient, self._ebGeneral)
  1311. d2 = self.loopback()
  1312. d = defer.gatherResults([d1, d2])
  1313. d.addCallback(lambda _: self.assertEqual(
  1314. self.statused,
  1315. {'MESSAGES': 9, 'UIDNEXT': '10', 'UNSEEN': 4}
  1316. ))
  1317. return d
  1318. def testFailedStatus(self):
  1319. def login():
  1320. return self.client.login(b'testuser', b'password-test')
  1321. def status():
  1322. return self.client.status('root/nonexistent', 'MESSAGES', 'UIDNEXT', 'UNSEEN')
  1323. def statused(result):
  1324. self.statused = result
  1325. def failed(failure):
  1326. self.failure = failure
  1327. self.statused = self.failure = None
  1328. d1 = self.connected.addCallback(strip(login))
  1329. d1.addCallbacks(strip(status), self._ebGeneral)
  1330. d1.addCallbacks(statused, failed)
  1331. d1.addCallbacks(self._cbStopClient, self._ebGeneral)
  1332. d2 = self.loopback()
  1333. return defer.gatherResults([d1, d2]).addCallback(self._cbTestFailedStatus)
  1334. def _cbTestFailedStatus(self, ignored):
  1335. self.assertEqual(
  1336. self.statused, None
  1337. )
  1338. self.assertEqual(
  1339. self.failure.value.args,
  1340. ('Could not open mailbox',)
  1341. )
  1342. def testFullAppend(self):
  1343. infile = util.sibpath(__file__, 'rfc822.message')
  1344. message = open(infile)
  1345. SimpleServer.theAccount.addMailbox('root/subthing')
  1346. def login():
  1347. return self.client.login(b'testuser', b'password-test')
  1348. def append():
  1349. return self.client.append(
  1350. 'root/subthing',
  1351. message,
  1352. ('\\SEEN', '\\DELETED'),
  1353. 'Tue, 17 Jun 2003 11:22:16 -0600 (MDT)',
  1354. )
  1355. d1 = self.connected.addCallback(strip(login))
  1356. d1.addCallbacks(strip(append), self._ebGeneral)
  1357. d1.addCallbacks(self._cbStopClient, self._ebGeneral)
  1358. d2 = self.loopback()
  1359. d = defer.gatherResults([d1, d2])
  1360. return d.addCallback(self._cbTestFullAppend, infile)
  1361. def _cbTestFullAppend(self, ignored, infile):
  1362. mb = SimpleServer.theAccount.mailboxes['ROOT/SUBTHING']
  1363. self.assertEqual(1, len(mb.messages))
  1364. self.assertEqual(
  1365. (['\\SEEN', '\\DELETED'], 'Tue, 17 Jun 2003 11:22:16 -0600 (MDT)', 0),
  1366. mb.messages[0][1:]
  1367. )
  1368. with open(infile) as f:
  1369. self.assertEqual(f.read(), mb.messages[0][0].getvalue())
  1370. def testPartialAppend(self):
  1371. infile = util.sibpath(__file__, 'rfc822.message')
  1372. SimpleServer.theAccount.addMailbox('PARTIAL/SUBTHING')
  1373. def login():
  1374. return self.client.login(b'testuser', b'password-test')
  1375. def append():
  1376. message = open(infile)
  1377. return self.client.sendCommand(
  1378. imap4.Command(
  1379. 'APPEND',
  1380. 'PARTIAL/SUBTHING (\\SEEN) "Right now" {%d}' % os.path.getsize(infile),
  1381. (), self.client._IMAP4Client__cbContinueAppend, message
  1382. )
  1383. )
  1384. d1 = self.connected.addCallback(strip(login))
  1385. d1.addCallbacks(strip(append), self._ebGeneral)
  1386. d1.addCallbacks(self._cbStopClient, self._ebGeneral)
  1387. d2 = self.loopback()
  1388. d = defer.gatherResults([d1, d2])
  1389. return d.addCallback(self._cbTestPartialAppend, infile)
  1390. def _cbTestPartialAppend(self, ignored, infile):
  1391. mb = SimpleServer.theAccount.mailboxes['PARTIAL/SUBTHING']
  1392. self.assertEqual(1, len(mb.messages))
  1393. self.assertEqual(
  1394. (['\\SEEN'], 'Right now', 0),
  1395. mb.messages[0][1:]
  1396. )
  1397. with open(infile) as f:
  1398. self.assertEqual(f.read(), mb.messages[0][0].getvalue())
  1399. def testCheck(self):
  1400. SimpleServer.theAccount.addMailbox(b'root/subthing')
  1401. def login():
  1402. return self.client.login(b'testuser', b'password-test')
  1403. def select():
  1404. return self.client.select(b'root/subthing')
  1405. def check():
  1406. return self.client.check()
  1407. d = self.connected.addCallback(strip(login))
  1408. d.addCallbacks(strip(select), self._ebGeneral)
  1409. d.addCallbacks(strip(check), self._ebGeneral)
  1410. d.addCallbacks(self._cbStopClient, self._ebGeneral)
  1411. return self.loopback()
  1412. # Okay, that was fun
  1413. def testClose(self):
  1414. m = SimpleMailbox()
  1415. m.messages = [
  1416. ('Message 1', ('\\Deleted', 'AnotherFlag'), None, 0),
  1417. ('Message 2', ('AnotherFlag',), None, 1),
  1418. ('Message 3', ('\\Deleted',), None, 2),
  1419. ]
  1420. SimpleServer.theAccount.addMailbox('mailbox', m)
  1421. def login():
  1422. return self.client.login(b'testuser', b'password-test')
  1423. def select():
  1424. return self.client.select(b'mailbox')
  1425. def close():
  1426. return self.client.close()
  1427. d = self.connected.addCallback(strip(login))
  1428. d.addCallbacks(strip(select), self._ebGeneral)
  1429. d.addCallbacks(strip(close), self._ebGeneral)
  1430. d.addCallbacks(self._cbStopClient, self._ebGeneral)
  1431. d2 = self.loopback()
  1432. return defer.gatherResults([d, d2]).addCallback(self._cbTestClose, m)
  1433. def _cbTestClose(self, ignored, m):
  1434. self.assertEqual(len(m.messages), 1)
  1435. self.assertEqual(m.messages[0], ('Message 2', ('AnotherFlag',), None, 1))
  1436. self.assertTrue(m.closed)
  1437. def testExpunge(self):
  1438. m = SimpleMailbox()
  1439. m.messages = [
  1440. ('Message 1', ('\\Deleted', 'AnotherFlag'), None, 0),
  1441. ('Message 2', ('AnotherFlag',), None, 1),
  1442. ('Message 3', ('\\Deleted',), None, 2),
  1443. ]
  1444. SimpleServer.theAccount.addMailbox('mailbox', m)
  1445. def login():
  1446. return self.client.login(b'testuser', b'password-test')
  1447. def select():
  1448. return self.client.select('mailbox')
  1449. def expunge():
  1450. return self.client.expunge()
  1451. def expunged(results):
  1452. self.assertFalse(self.server.mbox is None)
  1453. self.results = results
  1454. self.results = None
  1455. d1 = self.connected.addCallback(strip(login))
  1456. d1.addCallbacks(strip(select), self._ebGeneral)
  1457. d1.addCallbacks(strip(expunge), self._ebGeneral)
  1458. d1.addCallbacks(expunged, self._ebGeneral)
  1459. d1.addCallbacks(self._cbStopClient, self._ebGeneral)
  1460. d2 = self.loopback()
  1461. d = defer.gatherResults([d1, d2])
  1462. return d.addCallback(self._cbTestExpunge, m)
  1463. def _cbTestExpunge(self, ignored, m):
  1464. self.assertEqual(len(m.messages), 1)
  1465. self.assertEqual(m.messages[0], ('Message 2', ('AnotherFlag',), None, 1))
  1466. self.assertEqual(self.results, [0, 2])
  1467. class IMAP4ServerSearchTests(IMAP4HelperMixin, unittest.TestCase):
  1468. """
  1469. Tests for the behavior of the search_* functions in L{imap4.IMAP4Server}.
  1470. """
  1471. def setUp(self):
  1472. IMAP4HelperMixin.setUp(self)
  1473. self.earlierQuery = ["10-Dec-2009"]
  1474. self.sameDateQuery = ["13-Dec-2009"]
  1475. self.laterQuery = ["16-Dec-2009"]
  1476. self.seq = 0
  1477. self.msg = FakeyMessage({"date" : "Mon, 13 Dec 2009 21:25:10 GMT"}, [],
  1478. '13 Dec 2009 00:00:00 GMT', '', 1234, None)
  1479. def test_searchSentBefore(self):
  1480. """
  1481. L{imap4.IMAP4Server.search_SENTBEFORE} returns True if the message date
  1482. is earlier than the query date.
  1483. """
  1484. self.assertFalse(
  1485. self.server.search_SENTBEFORE(self.earlierQuery, self.seq, self.msg))
  1486. self.assertTrue(
  1487. self.server.search_SENTBEFORE(self.laterQuery, self.seq, self.msg))
  1488. def test_searchWildcard(self):
  1489. """
  1490. L{imap4.IMAP4Server.search_UID} returns True if the message UID is in
  1491. the search range.
  1492. """
  1493. self.assertFalse(
  1494. self.server.search_UID(['2:3'], self.seq, self.msg, (1, 1234)))
  1495. # 2:* should get translated to 2:<max UID> and then to 1:2
  1496. self.assertTrue(
  1497. self.server.search_UID(['2:*'], self.seq, self.msg, (1, 1234)))
  1498. self.assertTrue(
  1499. self.server.search_UID(['*'], self.seq, self.msg, (1, 1234)))
  1500. def test_searchWildcardHigh(self):
  1501. """
  1502. L{imap4.IMAP4Server.search_UID} should return True if there is a
  1503. wildcard, because a wildcard means "highest UID in the mailbox".
  1504. """
  1505. self.assertTrue(
  1506. self.server.search_UID(['1235:*'], self.seq, self.msg, (1234, 1)))
  1507. def test_reversedSearchTerms(self):
  1508. """
  1509. L{imap4.IMAP4Server.search_SENTON} returns True if the message date is
  1510. the same as the query date.
  1511. """
  1512. msgset = imap4.parseIdList('4:2')
  1513. self.assertEqual(list(msgset), [2, 3, 4])
  1514. def test_searchSentOn(self):
  1515. """
  1516. L{imap4.IMAP4Server.search_SENTON} returns True if the message date is
  1517. the same as the query date.
  1518. """
  1519. self.assertFalse(
  1520. self.server.search_SENTON(self.earlierQuery, self.seq, self.msg))
  1521. self.assertTrue(
  1522. self.server.search_SENTON(self.sameDateQuery, self.seq, self.msg))
  1523. self.assertFalse(
  1524. self.server.search_SENTON(self.laterQuery, self.seq, self.msg))
  1525. def test_searchSentSince(self):
  1526. """
  1527. L{imap4.IMAP4Server.search_SENTSINCE} returns True if the message date
  1528. is later than the query date.
  1529. """
  1530. self.assertTrue(
  1531. self.server.search_SENTSINCE(self.earlierQuery, self.seq, self.msg))
  1532. self.assertFalse(
  1533. self.server.search_SENTSINCE(self.laterQuery, self.seq, self.msg))
  1534. def test_searchOr(self):
  1535. """
  1536. L{imap4.IMAP4Server.search_OR} returns true if either of the two
  1537. expressions supplied to it returns true and returns false if neither
  1538. does.
  1539. """
  1540. self.assertTrue(
  1541. self.server.search_OR(
  1542. ["SENTSINCE"] + self.earlierQuery +
  1543. ["SENTSINCE"] + self.laterQuery,
  1544. self.seq, self.msg, (None, None)))
  1545. self.assertTrue(
  1546. self.server.search_OR(
  1547. ["SENTSINCE"] + self.laterQuery +
  1548. ["SENTSINCE"] + self.earlierQuery,
  1549. self.seq, self.msg, (None, None)))
  1550. self.assertFalse(
  1551. self.server.search_OR(
  1552. ["SENTON"] + self.laterQuery +
  1553. ["SENTSINCE"] + self.laterQuery,
  1554. self.seq, self.msg, (None, None)))
  1555. def test_searchNot(self):
  1556. """
  1557. L{imap4.IMAP4Server.search_NOT} returns the negation of the result
  1558. of the expression supplied to it.
  1559. """
  1560. self.assertFalse(self.server.search_NOT(
  1561. ["SENTSINCE"] + self.earlierQuery, self.seq, self.msg,
  1562. (None, None)))
  1563. self.assertTrue(self.server.search_NOT(
  1564. ["SENTON"] + self.laterQuery, self.seq, self.msg,
  1565. (None, None)))
  1566. def test_searchBefore(self):
  1567. """
  1568. L{imap4.IMAP4Server.search_BEFORE} returns True if the
  1569. internal message date is before the query date.
  1570. """
  1571. self.assertFalse(
  1572. self.server.search_BEFORE(self.earlierQuery, self.seq, self.msg))
  1573. self.assertFalse(
  1574. self.server.search_BEFORE(self.sameDateQuery, self.seq, self.msg))
  1575. self.assertTrue(
  1576. self.server.search_BEFORE(self.laterQuery, self.seq, self.msg))
  1577. def test_searchOn(self):
  1578. """
  1579. L{imap4.IMAP4Server.search_ON} returns True if the
  1580. internal message date is the same as the query date.
  1581. """
  1582. self.assertFalse(
  1583. self.server.search_ON(self.earlierQuery, self.seq, self.msg))
  1584. self.assertFalse(
  1585. self.server.search_ON(self.sameDateQuery, self.seq, self.msg))
  1586. self.assertFalse(
  1587. self.server.search_ON(self.laterQuery, self.seq, self.msg))
  1588. def test_searchSince(self):
  1589. """
  1590. L{imap4.IMAP4Server.search_SINCE} returns True if the
  1591. internal message date is greater than the query date.
  1592. """
  1593. self.assertTrue(
  1594. self.server.search_SINCE(self.earlierQuery, self.seq, self.msg))
  1595. self.assertTrue(
  1596. self.server.search_SINCE(self.sameDateQuery, self.seq, self.msg))
  1597. self.assertFalse(
  1598. self.server.search_SINCE(self.laterQuery, self.seq, self.msg))
  1599. class TestRealm:
  1600. theAccount = None
  1601. def requestAvatar(self, avatarId, mind, *interfaces):
  1602. return imap4.IAccount, self.theAccount, lambda: None
  1603. class TestChecker:
  1604. credentialInterfaces = (IUsernameHashedPassword, IUsernamePassword)
  1605. users = {
  1606. b'testuser': b'secret'
  1607. }
  1608. def requestAvatarId(self, credentials):
  1609. if credentials.username in self.users:
  1610. return defer.maybeDeferred(
  1611. credentials.checkPassword, self.users[credentials.username]
  1612. ).addCallback(self._cbCheck, credentials.username)
  1613. def _cbCheck(self, result, username):
  1614. if result:
  1615. return username
  1616. raise UnauthorizedLogin()
  1617. class AuthenticatorTests(IMAP4HelperMixin, unittest.TestCase):
  1618. def setUp(self):
  1619. IMAP4HelperMixin.setUp(self)
  1620. realm = TestRealm()
  1621. realm.theAccount = Account(b'testuser')
  1622. portal = Portal(realm)
  1623. portal.registerChecker(TestChecker())
  1624. self.server.portal = portal
  1625. self.authenticated = 0
  1626. self.account = realm.theAccount
  1627. def testCramMD5(self):
  1628. self.server.challengers[b'CRAM-MD5'] = CramMD5Credentials
  1629. cAuth = imap4.CramMD5ClientAuthenticator(b'testuser')
  1630. self.client.registerAuthenticator(cAuth)
  1631. def auth():
  1632. return self.client.authenticate(b'secret')
  1633. def authed():
  1634. self.authenticated = 1
  1635. d1 = self.connected.addCallback(strip(auth))
  1636. d1.addCallbacks(strip(authed), self._ebGeneral)
  1637. d1.addCallbacks(self._cbStopClient, self._ebGeneral)
  1638. d2 = self.loopback()
  1639. d = defer.gatherResults([d1, d2])
  1640. return d.addCallback(self._cbTestCramMD5)
  1641. def _cbTestCramMD5(self, ignored):
  1642. self.assertEqual(self.authenticated, 1)
  1643. self.assertEqual(self.server.account, self.account)
  1644. def testFailedCramMD5(self):
  1645. self.server.challengers[b'CRAM-MD5'] = CramMD5Credentials
  1646. cAuth = imap4.CramMD5ClientAuthenticator(b'testuser')
  1647. self.client.registerAuthenticator(cAuth)
  1648. def misauth():
  1649. return self.client.authenticate(b'not the secret')
  1650. def authed():
  1651. self.authenticated = 1
  1652. def misauthed():
  1653. self.authenticated = -1
  1654. d1 = self.connected.addCallback(strip(misauth))
  1655. d1.addCallbacks(strip(authed), strip(misauthed))
  1656. d1.addCallbacks(self._cbStopClient, self._ebGeneral)
  1657. d = defer.gatherResults([self.loopback(), d1])
  1658. return d.addCallback(self._cbTestFailedCramMD5)
  1659. def _cbTestFailedCramMD5(self, ignored):
  1660. self.assertEqual(self.authenticated, -1)
  1661. self.assertEqual(self.server.account, None)
  1662. def testLOGIN(self):
  1663. self.server.challengers[b'LOGIN'] = imap4.LOGINCredentials
  1664. cAuth = imap4.LOGINAuthenticator(b'testuser')
  1665. self.client.registerAuthenticator(cAuth)
  1666. def auth():
  1667. return self.client.authenticate(b'secret')
  1668. def authed():
  1669. self.authenticated = 1
  1670. d1 = self.connected.addCallback(strip(auth))
  1671. d1.addCallbacks(strip(authed), self._ebGeneral)
  1672. d1.addCallbacks(self._cbStopClient, self._ebGeneral)
  1673. d = defer.gatherResults([self.loopback(), d1])
  1674. return d.addCallback(self._cbTestLOGIN)
  1675. def _cbTestLOGIN(self, ignored):
  1676. self.assertEqual(self.authenticated, 1)
  1677. self.assertEqual(self.server.account, self.account)
  1678. def testFailedLOGIN(self):
  1679. self.server.challengers[b'LOGIN'] = imap4.LOGINCredentials
  1680. cAuth = imap4.LOGINAuthenticator(b'testuser')
  1681. self.client.registerAuthenticator(cAuth)
  1682. def misauth():
  1683. return self.client.authenticate(b'not the secret')
  1684. def authed():
  1685. self.authenticated = 1
  1686. def misauthed():
  1687. self.authenticated = -1
  1688. d1 = self.connected.addCallback(strip(misauth))
  1689. d1.addCallbacks(strip(authed), strip(misauthed))
  1690. d1.addCallbacks(self._cbStopClient, self._ebGeneral)
  1691. d = defer.gatherResults([self.loopback(), d1])
  1692. return d.addCallback(self._cbTestFailedLOGIN)
  1693. def _cbTestFailedLOGIN(self, ignored):
  1694. self.assertEqual(self.authenticated, -1)
  1695. self.assertEqual(self.server.account, None)
  1696. def testPLAIN(self):
  1697. self.server.challengers[b'PLAIN'] = imap4.PLAINCredentials
  1698. cAuth = imap4.PLAINAuthenticator(b'testuser')
  1699. self.client.registerAuthenticator(cAuth)
  1700. def auth():
  1701. return self.client.authenticate(b'secret')
  1702. def authed():
  1703. self.authenticated = 1
  1704. d1 = self.connected.addCallback(strip(auth))
  1705. d1.addCallbacks(strip(authed), self._ebGeneral)
  1706. d1.addCallbacks(self._cbStopClient, self._ebGeneral)
  1707. d = defer.gatherResults([self.loopback(), d1])
  1708. return d.addCallback(self._cbTestPLAIN)
  1709. def _cbTestPLAIN(self, ignored):
  1710. self.assertEqual(self.authenticated, 1)
  1711. self.assertEqual(self.server.account, self.account)
  1712. def testFailedPLAIN(self):
  1713. self.server.challengers[b'PLAIN'] = imap4.PLAINCredentials
  1714. cAuth = imap4.PLAINAuthenticator(b'testuser')
  1715. self.client.registerAuthenticator(cAuth)
  1716. def misauth():
  1717. return self.client.authenticate(b'not the secret')
  1718. def authed():
  1719. self.authenticated = 1
  1720. def misauthed():
  1721. self.authenticated = -1
  1722. d1 = self.connected.addCallback(strip(misauth))
  1723. d1.addCallbacks(strip(authed), strip(misauthed))
  1724. d1.addCallbacks(self._cbStopClient, self._ebGeneral)
  1725. d = defer.gatherResults([self.loopback(), d1])
  1726. return d.addCallback(self._cbTestFailedPLAIN)
  1727. def _cbTestFailedPLAIN(self, ignored):
  1728. self.assertEqual(self.authenticated, -1)
  1729. self.assertEqual(self.server.account, None)
  1730. class SASLPLAINTests(unittest.TestCase):
  1731. """
  1732. Tests for I{SASL PLAIN} authentication, as implemented by
  1733. L{imap4.PLAINAuthenticator} and L{imap4.PLAINCredentials}.
  1734. @see: U{http://www.faqs.org/rfcs/rfc2595.html}
  1735. @see: U{http://www.faqs.org/rfcs/rfc4616.html}
  1736. """
  1737. def test_authenticatorChallengeResponse(self):
  1738. """
  1739. L{PLAINAuthenticator.challengeResponse} returns challenge strings of
  1740. the form::
  1741. NUL<authn-id>NUL<secret>
  1742. """
  1743. username = b'testuser'
  1744. secret = b'secret'
  1745. chal = b'challenge'
  1746. cAuth = imap4.PLAINAuthenticator(username)
  1747. response = cAuth.challengeResponse(secret, chal)
  1748. self.assertEqual(response, b'\0' + username + b'\0' + secret)
  1749. def test_credentialsSetResponse(self):
  1750. """
  1751. L{PLAINCredentials.setResponse} parses challenge strings of the
  1752. form::
  1753. NUL<authn-id>NUL<secret>
  1754. """
  1755. cred = imap4.PLAINCredentials()
  1756. cred.setResponse(b'\0testuser\0secret')
  1757. self.assertEqual(cred.username, b'testuser')
  1758. self.assertEqual(cred.password, b'secret')
  1759. def test_credentialsInvalidResponse(self):
  1760. """
  1761. L{PLAINCredentials.setResponse} raises L{imap4.IllegalClientResponse}
  1762. when passed a string not of the expected form.
  1763. """
  1764. cred = imap4.PLAINCredentials()
  1765. self.assertRaises(
  1766. imap4.IllegalClientResponse, cred.setResponse, b'hello')
  1767. self.assertRaises(
  1768. imap4.IllegalClientResponse, cred.setResponse, b'hello\0world')
  1769. self.assertRaises(
  1770. imap4.IllegalClientResponse, cred.setResponse,
  1771. b'hello\0world\0Zoom!\0')
  1772. class UnsolicitedResponseTests(IMAP4HelperMixin, unittest.TestCase):
  1773. def testReadWrite(self):
  1774. def login():
  1775. return self.client.login(b'testuser', b'password-test')
  1776. def loggedIn():
  1777. self.server.modeChanged(1)
  1778. d1 = self.connected.addCallback(strip(login))
  1779. d1.addCallback(strip(loggedIn)).addErrback(self._ebGeneral)
  1780. d = defer.gatherResults([self.loopback(), d1])
  1781. return d.addCallback(self._cbTestReadWrite)
  1782. def _cbTestReadWrite(self, ignored):
  1783. E = self.client.events
  1784. self.assertEqual(E, [[b'modeChanged', 1]])
  1785. def testReadOnly(self):
  1786. def login():
  1787. return self.client.login(b'testuser', b'password-test')
  1788. def loggedIn():
  1789. self.server.modeChanged(0)
  1790. d1 = self.connected.addCallback(strip(login))
  1791. d1.addCallback(strip(loggedIn)).addErrback(self._ebGeneral)
  1792. d = defer.gatherResults([self.loopback(), d1])
  1793. return d.addCallback(self._cbTestReadOnly)
  1794. def _cbTestReadOnly(self, ignored):
  1795. E = self.client.events
  1796. self.assertEqual(E, [[b'modeChanged', 0]])
  1797. def testFlagChange(self):
  1798. flags = {
  1799. 1: [b'\\Answered', b'\\Deleted'],
  1800. 5: [],
  1801. 10: [b'\\Recent']
  1802. }
  1803. def login():
  1804. return self.client.login(b'testuser', b'password-test')
  1805. def loggedIn():
  1806. self.server.flagsChanged(flags)
  1807. d1 = self.connected.addCallback(strip(login))
  1808. d1.addCallback(strip(loggedIn)).addErrback(self._ebGeneral)
  1809. d = defer.gatherResults([self.loopback(), d1])
  1810. return d.addCallback(self._cbTestFlagChange, flags)
  1811. def _cbTestFlagChange(self, ignored, flags):
  1812. E = self.client.events
  1813. expect = [[b'flagsChanged', {x[0]: x[1]}] for x in flags.items()]
  1814. E.sort()
  1815. expect.sort()
  1816. self.assertEqual(E, expect)
  1817. def testNewMessages(self):
  1818. def login():
  1819. return self.client.login(b'testuser', b'password-test')
  1820. def loggedIn():
  1821. self.server.newMessages(10, None)
  1822. d1 = self.connected.addCallback(strip(login))
  1823. d1.addCallback(strip(loggedIn)).addErrback(self._ebGeneral)
  1824. d = defer.gatherResults([self.loopback(), d1])
  1825. return d.addCallback(self._cbTestNewMessages)
  1826. def _cbTestNewMessages(self, ignored):
  1827. E = self.client.events
  1828. self.assertEqual(E, [[b'newMessages', 10, None]])
  1829. def testNewRecentMessages(self):
  1830. def login():
  1831. return self.client.login(b'testuser', b'password-test')
  1832. def loggedIn():
  1833. self.server.newMessages(None, 10)
  1834. d1 = self.connected.addCallback(strip(login))
  1835. d1.addCallback(strip(loggedIn)).addErrback(self._ebGeneral)
  1836. d = defer.gatherResults([self.loopback(), d1])
  1837. return d.addCallback(self._cbTestNewRecentMessages)
  1838. def _cbTestNewRecentMessages(self, ignored):
  1839. E = self.client.events
  1840. self.assertEqual(E, [[b'newMessages', None, 10]])
  1841. def testNewMessagesAndRecent(self):
  1842. def login():
  1843. return self.client.login(b'testuser', b'password-test')
  1844. def loggedIn():
  1845. self.server.newMessages(20, 10)
  1846. d1 = self.connected.addCallback(strip(login))
  1847. d1.addCallback(strip(loggedIn)).addErrback(self._ebGeneral)
  1848. d = defer.gatherResults([self.loopback(), d1])
  1849. return d.addCallback(self._cbTestNewMessagesAndRecent)
  1850. def _cbTestNewMessagesAndRecent(self, ignored):
  1851. E = self.client.events
  1852. self.assertEqual(E, [[b'newMessages', 20, None], [b'newMessages', None, 10]])
  1853. class ClientCapabilityTests(unittest.TestCase):
  1854. """
  1855. Tests for issuance of the CAPABILITY command and handling of its response.
  1856. """
  1857. def setUp(self):
  1858. """
  1859. Create an L{imap4.IMAP4Client} connected to a L{StringTransport}.
  1860. """
  1861. self.transport = StringTransport()
  1862. self.protocol = imap4.IMAP4Client()
  1863. self.protocol.makeConnection(self.transport)
  1864. self.protocol.dataReceived(b'* OK [IMAP4rev1]\r\n')
  1865. def test_simpleAtoms(self):
  1866. """
  1867. A capability response consisting only of atoms without C{'='} in them
  1868. should result in a dict mapping those atoms to L{None}.
  1869. """
  1870. capabilitiesResult = self.protocol.getCapabilities(useCache=False)
  1871. self.protocol.dataReceived(b'* CAPABILITY IMAP4rev1 LOGINDISABLED\r\n')
  1872. self.protocol.dataReceived(b'0001 OK Capability completed.\r\n')
  1873. def gotCapabilities(capabilities):
  1874. self.assertEqual(
  1875. capabilities, {b'IMAP4rev1': None, b'LOGINDISABLED': None})
  1876. capabilitiesResult.addCallback(gotCapabilities)
  1877. return capabilitiesResult
  1878. def test_categoryAtoms(self):
  1879. """
  1880. A capability response consisting of atoms including C{'='} should have
  1881. those atoms split on that byte and have capabilities in the same
  1882. category aggregated into lists in the resulting dictionary.
  1883. (n.b. - I made up the word "category atom"; the protocol has no notion
  1884. of structure here, but rather allows each capability to define the
  1885. semantics of its entry in the capability response in a freeform manner.
  1886. If I had realized this earlier, the API for capabilities would look
  1887. different. As it is, we can hope that no one defines any crazy
  1888. semantics which are incompatible with this API, or try to figure out a
  1889. better API when someone does. -exarkun)
  1890. """
  1891. capabilitiesResult = self.protocol.getCapabilities(useCache=False)
  1892. self.protocol.dataReceived(b'* CAPABILITY IMAP4rev1 AUTH=LOGIN AUTH=PLAIN\r\n')
  1893. self.protocol.dataReceived(b'0001 OK Capability completed.\r\n')
  1894. def gotCapabilities(capabilities):
  1895. self.assertEqual(
  1896. capabilities, {b'IMAP4rev1': None, b'AUTH': [b'LOGIN', b'PLAIN']})
  1897. capabilitiesResult.addCallback(gotCapabilities)
  1898. return capabilitiesResult
  1899. def test_mixedAtoms(self):
  1900. """
  1901. A capability response consisting of both simple and category atoms of
  1902. the same type should result in a list containing L{None} as well as the
  1903. values for the category.
  1904. """
  1905. capabilitiesResult = self.protocol.getCapabilities(useCache=False)
  1906. # Exercise codepath for both orderings of =-having and =-missing
  1907. # capabilities.
  1908. self.protocol.dataReceived(
  1909. b'* CAPABILITY IMAP4rev1 FOO FOO=BAR BAR=FOO BAR\r\n')
  1910. self.protocol.dataReceived(b'0001 OK Capability completed.\r\n')
  1911. def gotCapabilities(capabilities):
  1912. self.assertEqual(capabilities, {b'IMAP4rev1': None,
  1913. b'FOO': [None, b'BAR'],
  1914. b'BAR': [b'FOO', None]})
  1915. capabilitiesResult.addCallback(gotCapabilities)
  1916. return capabilitiesResult
  1917. class StillSimplerClient(imap4.IMAP4Client):
  1918. """
  1919. An IMAP4 client which keeps track of unsolicited flag changes.
  1920. """
  1921. def __init__(self):
  1922. imap4.IMAP4Client.__init__(self)
  1923. self.flags = {}
  1924. def flagsChanged(self, newFlags):
  1925. self.flags.update(newFlags)
  1926. class HandCraftedTests(IMAP4HelperMixin, unittest.TestCase):
  1927. def testTrailingLiteral(self):
  1928. transport = StringTransport()
  1929. c = imap4.IMAP4Client()
  1930. c.makeConnection(transport)
  1931. c.lineReceived(b'* OK [IMAP4rev1]')
  1932. def cbSelect(ignored):
  1933. d = c.fetchMessage('1')
  1934. c.dataReceived(b'* 1 FETCH (RFC822 {10}\r\n0123456789\r\n RFC822.SIZE 10)\r\n')
  1935. c.dataReceived(b'0003 OK FETCH\r\n')
  1936. return d
  1937. def cbLogin(ignored):
  1938. d = c.select('inbox')
  1939. c.lineReceived(b'0002 OK SELECT')
  1940. d.addCallback(cbSelect)
  1941. return d
  1942. d = c.login('blah', 'blah')
  1943. c.dataReceived(b'0001 OK LOGIN\r\n')
  1944. d.addCallback(cbLogin)
  1945. return d
  1946. def testPathelogicalScatteringOfLiterals(self):
  1947. self.server.checker.addUser(b'testuser', b'password-test')
  1948. transport = StringTransport()
  1949. self.server.makeConnection(transport)
  1950. transport.clear()
  1951. self.server.dataReceived(b"01 LOGIN {8}\r\n")
  1952. self.assertEqual(transport.value(), b"+ Ready for 8 octets of text\r\n")
  1953. transport.clear()
  1954. self.server.dataReceived(b"testuser {13}\r\n")
  1955. self.assertEqual(transport.value(), b"+ Ready for 13 octets of text\r\n")
  1956. transport.clear()
  1957. self.server.dataReceived(b"password-test\r\n")
  1958. self.assertEqual(transport.value(), b"01 OK LOGIN succeeded\r\n")
  1959. self.assertEqual(self.server.state, 'auth')
  1960. self.server.connectionLost(error.ConnectionDone("Connection done."))
  1961. def test_unsolicitedResponseMixedWithSolicitedResponse(self):
  1962. """
  1963. If unsolicited data is received along with solicited data in the
  1964. response to a I{FETCH} command issued by L{IMAP4Client.fetchSpecific},
  1965. the unsolicited data is passed to the appropriate callback and not
  1966. included in the result with which the L{Deferred} returned by
  1967. L{IMAP4Client.fetchSpecific} fires.
  1968. """
  1969. transport = StringTransport()
  1970. c = StillSimplerClient()
  1971. c.makeConnection(transport)
  1972. c.lineReceived(b'* OK [IMAP4rev1]')
  1973. def login():
  1974. d = c.login('blah', 'blah')
  1975. c.dataReceived(b'0001 OK LOGIN\r\n')
  1976. return d
  1977. def select():
  1978. d = c.select('inbox')
  1979. c.lineReceived(b'0002 OK SELECT')
  1980. return d
  1981. def fetch():
  1982. d = c.fetchSpecific(b'1:*',
  1983. headerType = b'HEADER.FIELDS',
  1984. headerArgs = [b'SUBJECT'])
  1985. c.dataReceived(b'* 1 FETCH (BODY[HEADER.FIELDS ("SUBJECT")] {38}\r\n')
  1986. c.dataReceived(b'Subject: Suprise for your woman...\r\n')
  1987. c.dataReceived(b'\r\n')
  1988. c.dataReceived(b')\r\n')
  1989. c.dataReceived(b'* 1 FETCH (FLAGS (\Seen))\r\n')
  1990. c.dataReceived(b'* 2 FETCH (BODY[HEADER.FIELDS ("SUBJECT")] {75}\r\n')
  1991. c.dataReceived(b'Subject: What you been doing. Order your meds here . ,. handcuff madsen\r\n')
  1992. c.dataReceived(b'\r\n')
  1993. c.dataReceived(b')\r\n')
  1994. c.dataReceived(b'0003 OK FETCH completed\r\n')
  1995. return d
  1996. def test(res):
  1997. self.assertEqual(res, {
  1998. 1: [[b'BODY', ['HEADER.FIELDS', ['SUBJECT']],
  1999. b'Subject: Suprise for your woman...\r\n\r\n']],
  2000. 2: [[b'BODY', [b'HEADER.FIELDS', [b'SUBJECT']],
  2001. b'Subject: What you been doing. Order your meds here . ,. handcuff madsen\r\n\r\n']]
  2002. })
  2003. self.assertEqual(c.flags, {1: ['\\Seen']})
  2004. return login(
  2005. ).addCallback(strip(select)
  2006. ).addCallback(strip(fetch)
  2007. ).addCallback(test)
  2008. def test_literalWithoutPrecedingWhitespace(self):
  2009. """
  2010. Literals should be recognized even when they are not preceded by
  2011. whitespace.
  2012. """
  2013. transport = StringTransport()
  2014. protocol = imap4.IMAP4Client()
  2015. protocol.makeConnection(transport)
  2016. protocol.lineReceived(b'* OK [IMAP4rev1]')
  2017. def login():
  2018. d = protocol.login(b'blah', b'blah')
  2019. protocol.dataReceived(b'0001 OK LOGIN\r\n')
  2020. return d
  2021. def select():
  2022. d = protocol.select(b'inbox')
  2023. protocol.lineReceived(b'0002 OK SELECT')
  2024. return d
  2025. def fetch():
  2026. d = protocol.fetchSpecific(b'1:*',
  2027. headerType=b'HEADER.FIELDS',
  2028. headerArgs=[b'SUBJECT'])
  2029. protocol.dataReceived(
  2030. b'* 1 FETCH (BODY[HEADER.FIELDS ({7}\r\nSUBJECT)] "Hello")\r\n')
  2031. protocol.dataReceived(b'0003 OK FETCH completed\r\n')
  2032. return d
  2033. def test(result):
  2034. self.assertEqual(
  2035. result, {1: [[b'BODY', [b'HEADER.FIELDS', [b'SUBJECT']], b'Hello']]})
  2036. d = login()
  2037. d.addCallback(strip(select))
  2038. d.addCallback(strip(fetch))
  2039. d.addCallback(test)
  2040. return d
  2041. def test_nonIntegerLiteralLength(self):
  2042. """
  2043. If the server sends a literal length which cannot be parsed as an
  2044. integer, L{IMAP4Client.lineReceived} should cause the protocol to be
  2045. disconnected by raising L{imap4.IllegalServerResponse}.
  2046. """
  2047. transport = StringTransport()
  2048. protocol = imap4.IMAP4Client()
  2049. protocol.makeConnection(transport)
  2050. protocol.lineReceived(b'* OK [IMAP4rev1]')
  2051. def login():
  2052. d = protocol.login('blah', 'blah')
  2053. protocol.dataReceived(b'0001 OK LOGIN\r\n')
  2054. return d
  2055. def select():
  2056. d = protocol.select('inbox')
  2057. protocol.lineReceived(b'0002 OK SELECT')
  2058. return d
  2059. def fetch():
  2060. protocol.fetchSpecific(
  2061. b'1:*',
  2062. headerType=b'HEADER.FIELDS',
  2063. headerArgs=[b'SUBJECT'])
  2064. self.assertRaises(
  2065. imap4.IllegalServerResponse,
  2066. protocol.dataReceived,
  2067. b'* 1 FETCH {xyz}\r\n...')
  2068. d = login()
  2069. d.addCallback(strip(select))
  2070. d.addCallback(strip(fetch))
  2071. return d
  2072. def test_flagsChangedInsideFetchSpecificResponse(self):
  2073. """
  2074. Any unrequested flag information received along with other requested
  2075. information in an untagged I{FETCH} received in response to a request
  2076. issued with L{IMAP4Client.fetchSpecific} is passed to the
  2077. C{flagsChanged} callback.
  2078. """
  2079. transport = StringTransport()
  2080. c = StillSimplerClient()
  2081. c.makeConnection(transport)
  2082. c.lineReceived(b'* OK [IMAP4rev1]')
  2083. def login():
  2084. d = c.login('blah', 'blah')
  2085. c.dataReceived(b'0001 OK LOGIN\r\n')
  2086. return d
  2087. def select():
  2088. d = c.select('inbox')
  2089. c.lineReceived(b'0002 OK SELECT')
  2090. return d
  2091. def fetch():
  2092. d = c.fetchSpecific(b'1:*',
  2093. headerType=b'HEADER.FIELDS',
  2094. headerArgs=[b'SUBJECT'])
  2095. # This response includes FLAGS after the requested data.
  2096. c.dataReceived(b'* 1 FETCH (BODY[HEADER.FIELDS ("SUBJECT")] {22}\r\n')
  2097. c.dataReceived(b'Subject: subject one\r\n')
  2098. c.dataReceived(b' FLAGS (\\Recent))\r\n')
  2099. # And this one includes it before! Either is possible.
  2100. c.dataReceived(b'* 2 FETCH (FLAGS (\\Seen) BODY[HEADER.FIELDS ("SUBJECT")] {22}\r\n')
  2101. c.dataReceived(b'Subject: subject two\r\n')
  2102. c.dataReceived(b')\r\n')
  2103. c.dataReceived(b'0003 OK FETCH completed\r\n')
  2104. return d
  2105. def test(res):
  2106. self.assertEqual(res, {
  2107. 1: [['BODY', ['HEADER.FIELDS', ['SUBJECT']],
  2108. 'Subject: subject one\r\n']],
  2109. 2: [['BODY', ['HEADER.FIELDS', ['SUBJECT']],
  2110. 'Subject: subject two\r\n']]
  2111. })
  2112. self.assertEqual(c.flags, {1: ['\\Recent'], 2: ['\\Seen']})
  2113. return login(
  2114. ).addCallback(strip(select)
  2115. ).addCallback(strip(fetch)
  2116. ).addCallback(test)
  2117. def test_flagsChangedInsideFetchMessageResponse(self):
  2118. """
  2119. Any unrequested flag information received along with other requested
  2120. information in an untagged I{FETCH} received in response to a request
  2121. issued with L{IMAP4Client.fetchMessage} is passed to the
  2122. C{flagsChanged} callback.
  2123. """
  2124. transport = StringTransport()
  2125. c = StillSimplerClient()
  2126. c.makeConnection(transport)
  2127. c.lineReceived(b'* OK [IMAP4rev1]')
  2128. def login():
  2129. d = c.login('blah', 'blah')
  2130. c.dataReceived(b'0001 OK LOGIN\r\n')
  2131. return d
  2132. def select():
  2133. d = c.select('inbox')
  2134. c.lineReceived(b'0002 OK SELECT')
  2135. return d
  2136. def fetch():
  2137. d = c.fetchMessage('1:*')
  2138. c.dataReceived(b'* 1 FETCH (RFC822 {24}\r\n')
  2139. c.dataReceived(b'Subject: first subject\r\n')
  2140. c.dataReceived(b' FLAGS (\Seen))\r\n')
  2141. c.dataReceived(b'* 2 FETCH (FLAGS (\Recent \Seen) RFC822 {25}\r\n')
  2142. c.dataReceived(b'Subject: second subject\r\n')
  2143. c.dataReceived(b')\r\n')
  2144. c.dataReceived(b'0003 OK FETCH completed\r\n')
  2145. return d
  2146. def test(res):
  2147. self.assertEqual(res, {
  2148. 1: {'RFC822': 'Subject: first subject\r\n'},
  2149. 2: {'RFC822': 'Subject: second subject\r\n'}})
  2150. self.assertEqual(
  2151. c.flags, {1: ['\\Seen'], 2: ['\\Recent', '\\Seen']})
  2152. return login(
  2153. ).addCallback(strip(select)
  2154. ).addCallback(strip(fetch)
  2155. ).addCallback(test)
  2156. def test_authenticationChallengeDecodingException(self):
  2157. """
  2158. When decoding a base64 encoded authentication message from the server,
  2159. decoding errors are logged and then the client closes the connection.
  2160. """
  2161. transport = StringTransportWithDisconnection()
  2162. protocol = imap4.IMAP4Client()
  2163. transport.protocol = protocol
  2164. protocol.makeConnection(transport)
  2165. protocol.lineReceived(
  2166. b'* OK [CAPABILITY IMAP4rev1 IDLE NAMESPACE AUTH=CRAM-MD5] '
  2167. b'Twisted IMAP4rev1 Ready')
  2168. cAuth = imap4.CramMD5ClientAuthenticator(b'testuser')
  2169. protocol.registerAuthenticator(cAuth)
  2170. d = protocol.authenticate('secret')
  2171. # Should really be something describing the base64 decode error. See
  2172. # #6021.
  2173. self.assertFailure(d, error.ConnectionDone)
  2174. protocol.dataReceived(b'+ Something bad! and bad\r\n')
  2175. # This should not really be logged. See #6021.
  2176. logged = self.flushLoggedErrors(imap4.IllegalServerResponse)
  2177. self.assertEqual(len(logged), 1)
  2178. self.assertEqual(logged[0].value.args[0], "Something bad! and bad")
  2179. return d
  2180. class PreauthIMAP4ClientMixin:
  2181. """
  2182. Mixin for L{unittest.TestCase} subclasses which provides a C{setUp} method
  2183. which creates an L{IMAP4Client} connected to a L{StringTransport} and puts
  2184. it into the I{authenticated} state.
  2185. @ivar transport: A L{StringTransport} to which C{client} is connected.
  2186. @ivar client: An L{IMAP4Client} which is connected to C{transport}.
  2187. """
  2188. clientProtocol = imap4.IMAP4Client
  2189. def setUp(self):
  2190. """
  2191. Create an IMAP4Client connected to a fake transport and in the
  2192. authenticated state.
  2193. """
  2194. self.transport = StringTransport()
  2195. self.client = self.clientProtocol()
  2196. self.client.makeConnection(self.transport)
  2197. self.client.dataReceived(b'* PREAUTH Hello unittest\r\n')
  2198. def _extractDeferredResult(self, d):
  2199. """
  2200. Synchronously extract the result of the given L{Deferred}. Fail the
  2201. test if that is not possible.
  2202. """
  2203. result = []
  2204. error = []
  2205. d.addCallbacks(result.append, error.append)
  2206. if result:
  2207. return result[0]
  2208. elif error:
  2209. error[0].raiseException()
  2210. else:
  2211. self.fail("Expected result not available")
  2212. class SelectionTestsMixin(PreauthIMAP4ClientMixin):
  2213. """
  2214. Mixin for test cases which defines tests which apply to both I{EXAMINE} and
  2215. I{SELECT} support.
  2216. """
  2217. def _examineOrSelect(self):
  2218. """
  2219. Issue either an I{EXAMINE} or I{SELECT} command (depending on
  2220. C{self.method}), assert that the correct bytes are written to the
  2221. transport, and return the L{Deferred} returned by whichever method was
  2222. called.
  2223. """
  2224. d = getattr(self.client, self.method)('foobox')
  2225. self.assertEqual(
  2226. self.transport.value(), b'0001 ' + self.command + b' foobox\r\n')
  2227. return d
  2228. def _response(self, *lines):
  2229. """
  2230. Deliver the given (unterminated) response lines to C{self.client} and
  2231. then deliver a tagged SELECT or EXAMINE completion line to finish the
  2232. SELECT or EXAMINE response.
  2233. """
  2234. for line in lines:
  2235. self.client.dataReceived(line + b'\r\n')
  2236. self.client.dataReceived(
  2237. b'0001 OK [READ-ONLY] %s completed\r\n' % (self.command,))
  2238. def test_exists(self):
  2239. """
  2240. If the server response to a I{SELECT} or I{EXAMINE} command includes an
  2241. I{EXISTS} response, the L{Deferred} return by L{IMAP4Client.select} or
  2242. L{IMAP4Client.examine} fires with a C{dict} including the value
  2243. associated with the C{'EXISTS'} key.
  2244. """
  2245. d = self._examineOrSelect()
  2246. self._response('* 3 EXISTS')
  2247. self.assertEqual(
  2248. self._extractDeferredResult(d),
  2249. {'READ-WRITE': False, 'EXISTS': 3})
  2250. def test_nonIntegerExists(self):
  2251. """
  2252. If the server returns a non-integer EXISTS value in its response to a
  2253. I{SELECT} or I{EXAMINE} command, the L{Deferred} returned by
  2254. L{IMAP4Client.select} or L{IMAP4Client.examine} fails with
  2255. L{IllegalServerResponse}.
  2256. """
  2257. d = self._examineOrSelect()
  2258. self._response('* foo EXISTS')
  2259. self.assertRaises(
  2260. imap4.IllegalServerResponse, self._extractDeferredResult, d)
  2261. def test_recent(self):
  2262. """
  2263. If the server response to a I{SELECT} or I{EXAMINE} command includes an
  2264. I{RECENT} response, the L{Deferred} return by L{IMAP4Client.select} or
  2265. L{IMAP4Client.examine} fires with a C{dict} including the value
  2266. associated with the C{'RECENT'} key.
  2267. """
  2268. d = self._examineOrSelect()
  2269. self._response('* 5 RECENT')
  2270. self.assertEqual(
  2271. self._extractDeferredResult(d),
  2272. {'READ-WRITE': False, 'RECENT': 5})
  2273. def test_nonIntegerRecent(self):
  2274. """
  2275. If the server returns a non-integer RECENT value in its response to a
  2276. I{SELECT} or I{EXAMINE} command, the L{Deferred} returned by
  2277. L{IMAP4Client.select} or L{IMAP4Client.examine} fails with
  2278. L{IllegalServerResponse}.
  2279. """
  2280. d = self._examineOrSelect()
  2281. self._response('* foo RECENT')
  2282. self.assertRaises(
  2283. imap4.IllegalServerResponse, self._extractDeferredResult, d)
  2284. def test_unseen(self):
  2285. """
  2286. If the server response to a I{SELECT} or I{EXAMINE} command includes an
  2287. I{UNSEEN} response, the L{Deferred} returned by L{IMAP4Client.select} or
  2288. L{IMAP4Client.examine} fires with a C{dict} including the value
  2289. associated with the C{'UNSEEN'} key.
  2290. """
  2291. d = self._examineOrSelect()
  2292. self._response('* OK [UNSEEN 8] Message 8 is first unseen')
  2293. self.assertEqual(
  2294. self._extractDeferredResult(d),
  2295. {'READ-WRITE': False, 'UNSEEN': 8})
  2296. def test_nonIntegerUnseen(self):
  2297. """
  2298. If the server returns a non-integer UNSEEN value in its response to a
  2299. I{SELECT} or I{EXAMINE} command, the L{Deferred} returned by
  2300. L{IMAP4Client.select} or L{IMAP4Client.examine} fails with
  2301. L{IllegalServerResponse}.
  2302. """
  2303. d = self._examineOrSelect()
  2304. self._response('* OK [UNSEEN foo] Message foo is first unseen')
  2305. self.assertRaises(
  2306. imap4.IllegalServerResponse, self._extractDeferredResult, d)
  2307. def test_uidvalidity(self):
  2308. """
  2309. If the server response to a I{SELECT} or I{EXAMINE} command includes an
  2310. I{UIDVALIDITY} response, the L{Deferred} returned by
  2311. L{IMAP4Client.select} or L{IMAP4Client.examine} fires with a C{dict}
  2312. including the value associated with the C{'UIDVALIDITY'} key.
  2313. """
  2314. d = self._examineOrSelect()
  2315. self._response('* OK [UIDVALIDITY 12345] UIDs valid')
  2316. self.assertEqual(
  2317. self._extractDeferredResult(d),
  2318. {'READ-WRITE': False, 'UIDVALIDITY': 12345})
  2319. def test_nonIntegerUIDVALIDITY(self):
  2320. """
  2321. If the server returns a non-integer UIDVALIDITY value in its response to
  2322. a I{SELECT} or I{EXAMINE} command, the L{Deferred} returned by
  2323. L{IMAP4Client.select} or L{IMAP4Client.examine} fails with
  2324. L{IllegalServerResponse}.
  2325. """
  2326. d = self._examineOrSelect()
  2327. self._response('* OK [UIDVALIDITY foo] UIDs valid')
  2328. self.assertRaises(
  2329. imap4.IllegalServerResponse, self._extractDeferredResult, d)
  2330. def test_uidnext(self):
  2331. """
  2332. If the server response to a I{SELECT} or I{EXAMINE} command includes an
  2333. I{UIDNEXT} response, the L{Deferred} returned by L{IMAP4Client.select}
  2334. or L{IMAP4Client.examine} fires with a C{dict} including the value
  2335. associated with the C{'UIDNEXT'} key.
  2336. """
  2337. d = self._examineOrSelect()
  2338. self._response('* OK [UIDNEXT 4392] Predicted next UID')
  2339. self.assertEqual(
  2340. self._extractDeferredResult(d),
  2341. {'READ-WRITE': False, 'UIDNEXT': 4392})
  2342. def test_nonIntegerUIDNEXT(self):
  2343. """
  2344. If the server returns a non-integer UIDNEXT value in its response to a
  2345. I{SELECT} or I{EXAMINE} command, the L{Deferred} returned by
  2346. L{IMAP4Client.select} or L{IMAP4Client.examine} fails with
  2347. L{IllegalServerResponse}.
  2348. """
  2349. d = self._examineOrSelect()
  2350. self._response('* OK [UIDNEXT foo] Predicted next UID')
  2351. self.assertRaises(
  2352. imap4.IllegalServerResponse, self._extractDeferredResult, d)
  2353. def test_flags(self):
  2354. """
  2355. If the server response to a I{SELECT} or I{EXAMINE} command includes an
  2356. I{FLAGS} response, the L{Deferred} returned by L{IMAP4Client.select} or
  2357. L{IMAP4Client.examine} fires with a C{dict} including the value
  2358. associated with the C{'FLAGS'} key.
  2359. """
  2360. d = self._examineOrSelect()
  2361. self._response(
  2362. '* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)')
  2363. self.assertEqual(
  2364. self._extractDeferredResult(d), {
  2365. 'READ-WRITE': False,
  2366. 'FLAGS': ('\\Answered', '\\Flagged', '\\Deleted', '\\Seen',
  2367. '\\Draft')})
  2368. def test_permanentflags(self):
  2369. """
  2370. If the server response to a I{SELECT} or I{EXAMINE} command includes an
  2371. I{FLAGS} response, the L{Deferred} returned by L{IMAP4Client.select} or
  2372. L{IMAP4Client.examine} fires with a C{dict} including the value
  2373. associated with the C{'FLAGS'} key.
  2374. """
  2375. d = self._examineOrSelect()
  2376. self._response(
  2377. '* OK [PERMANENTFLAGS (\\Starred)] Just one permanent flag in '
  2378. 'that list up there')
  2379. self.assertEqual(
  2380. self._extractDeferredResult(d), {
  2381. 'READ-WRITE': False,
  2382. 'PERMANENTFLAGS': ('\\Starred',)})
  2383. def test_unrecognizedOk(self):
  2384. """
  2385. If the server response to a I{SELECT} or I{EXAMINE} command includes an
  2386. I{OK} with unrecognized response code text, parsing does not fail.
  2387. """
  2388. d = self._examineOrSelect()
  2389. self._response(
  2390. '* OK [X-MADE-UP] I just made this response text up.')
  2391. # The value won't show up in the result. It would be okay if it did
  2392. # someday, perhaps. This shouldn't ever happen, though.
  2393. self.assertEqual(
  2394. self._extractDeferredResult(d), {'READ-WRITE': False})
  2395. def test_bareOk(self):
  2396. """
  2397. If the server response to a I{SELECT} or I{EXAMINE} command includes an
  2398. I{OK} with no response code text, parsing does not fail.
  2399. """
  2400. d = self._examineOrSelect()
  2401. self._response('* OK')
  2402. self.assertEqual(
  2403. self._extractDeferredResult(d), {'READ-WRITE': False})
  2404. class IMAP4ClientExamineTests(SelectionTestsMixin, unittest.TestCase):
  2405. """
  2406. Tests for the L{IMAP4Client.examine} method.
  2407. An example of usage of the EXAMINE command from RFC 3501, section 6.3.2::
  2408. S: * 17 EXISTS
  2409. S: * 2 RECENT
  2410. S: * OK [UNSEEN 8] Message 8 is first unseen
  2411. S: * OK [UIDVALIDITY 3857529045] UIDs valid
  2412. S: * OK [UIDNEXT 4392] Predicted next UID
  2413. S: * FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)
  2414. S: * OK [PERMANENTFLAGS ()] No permanent flags permitted
  2415. S: A932 OK [READ-ONLY] EXAMINE completed
  2416. """
  2417. method = 'examine'
  2418. command = 'EXAMINE'
  2419. class IMAP4ClientSelectTests(SelectionTestsMixin, unittest.TestCase):
  2420. """
  2421. Tests for the L{IMAP4Client.select} method.
  2422. An example of usage of the SELECT command from RFC 3501, section 6.3.1::
  2423. C: A142 SELECT INBOX
  2424. S: * 172 EXISTS
  2425. S: * 1 RECENT
  2426. S: * OK [UNSEEN 12] Message 12 is first unseen
  2427. S: * OK [UIDVALIDITY 3857529045] UIDs valid
  2428. S: * OK [UIDNEXT 4392] Predicted next UID
  2429. S: * FLAGS (\Answered \Flagged \Deleted \Seen \Draft)
  2430. S: * OK [PERMANENTFLAGS (\Deleted \Seen \*)] Limited
  2431. S: A142 OK [READ-WRITE] SELECT completed
  2432. """
  2433. method = 'select'
  2434. command = 'SELECT'
  2435. class IMAP4ClientExpungeTests(PreauthIMAP4ClientMixin, unittest.TestCase):
  2436. """
  2437. Tests for the L{IMAP4Client.expunge} method.
  2438. An example of usage of the EXPUNGE command from RFC 3501, section 6.4.3::
  2439. C: A202 EXPUNGE
  2440. S: * 3 EXPUNGE
  2441. S: * 3 EXPUNGE
  2442. S: * 5 EXPUNGE
  2443. S: * 8 EXPUNGE
  2444. S: A202 OK EXPUNGE completed
  2445. """
  2446. def _expunge(self):
  2447. d = self.client.expunge()
  2448. self.assertEqual(self.transport.value(), b'0001 EXPUNGE\r\n')
  2449. self.transport.clear()
  2450. return d
  2451. def _response(self, sequenceNumbers):
  2452. for number in sequenceNumbers:
  2453. self.client.lineReceived(b'* %s EXPUNGE' % (number,))
  2454. self.client.lineReceived(b'0001 OK EXPUNGE COMPLETED')
  2455. def test_expunge(self):
  2456. """
  2457. L{IMAP4Client.expunge} sends the I{EXPUNGE} command and returns a
  2458. L{Deferred} which fires with a C{list} of message sequence numbers
  2459. given by the server's response.
  2460. """
  2461. d = self._expunge()
  2462. self._response([3, 3, 5, 8])
  2463. self.assertEqual(self._extractDeferredResult(d), [3, 3, 5, 8])
  2464. def test_nonIntegerExpunged(self):
  2465. """
  2466. If the server responds with a non-integer where a message sequence
  2467. number is expected, the L{Deferred} returned by L{IMAP4Client.expunge}
  2468. fails with L{IllegalServerResponse}.
  2469. """
  2470. d = self._expunge()
  2471. self._response([3, 3, 'foo', 8])
  2472. self.assertRaises(
  2473. imap4.IllegalServerResponse, self._extractDeferredResult, d)
  2474. class IMAP4ClientSearchTests(PreauthIMAP4ClientMixin, unittest.TestCase):
  2475. """
  2476. Tests for the L{IMAP4Client.search} method.
  2477. An example of usage of the SEARCH command from RFC 3501, section 6.4.4::
  2478. C: A282 SEARCH FLAGGED SINCE 1-Feb-1994 NOT FROM "Smith"
  2479. S: * SEARCH 2 84 882
  2480. S: A282 OK SEARCH completed
  2481. C: A283 SEARCH TEXT "string not in mailbox"
  2482. S: * SEARCH
  2483. S: A283 OK SEARCH completed
  2484. C: A284 SEARCH CHARSET UTF-8 TEXT {6}
  2485. C: XXXXXX
  2486. S: * SEARCH 43
  2487. S: A284 OK SEARCH completed
  2488. """
  2489. def _search(self):
  2490. d = self.client.search(imap4.Query(text="ABCDEF"))
  2491. self.assertEqual(
  2492. self.transport.value(), b'0001 SEARCH (TEXT "ABCDEF")\r\n')
  2493. return d
  2494. def _response(self, messageNumbers):
  2495. self.client.lineReceived(
  2496. b"* SEARCH " + b" ".join(map(str, messageNumbers)))
  2497. self.client.lineReceived(b"0001 OK SEARCH completed")
  2498. def test_search(self):
  2499. """
  2500. L{IMAP4Client.search} sends the I{SEARCH} command and returns a
  2501. L{Deferred} which fires with a C{list} of message sequence numbers
  2502. given by the server's response.
  2503. """
  2504. d = self._search()
  2505. self._response([2, 5, 10])
  2506. self.assertEqual(self._extractDeferredResult(d), [2, 5, 10])
  2507. def test_nonIntegerFound(self):
  2508. """
  2509. If the server responds with a non-integer where a message sequence
  2510. number is expected, the L{Deferred} returned by L{IMAP4Client.search}
  2511. fails with L{IllegalServerResponse}.
  2512. """
  2513. d = self._search()
  2514. self._response([2, "foo", 10])
  2515. self.assertRaises(
  2516. imap4.IllegalServerResponse, self._extractDeferredResult, d)
  2517. class IMAP4ClientFetchTests(PreauthIMAP4ClientMixin, unittest.TestCase):
  2518. """
  2519. Tests for the L{IMAP4Client.fetch} method.
  2520. See RFC 3501, section 6.4.5.
  2521. """
  2522. def test_fetchUID(self):
  2523. """
  2524. L{IMAP4Client.fetchUID} sends the I{FETCH UID} command and returns a
  2525. L{Deferred} which fires with a C{dict} mapping message sequence numbers
  2526. to C{dict}s mapping C{'UID'} to that message's I{UID} in the server's
  2527. response.
  2528. """
  2529. d = self.client.fetchUID(b'1:7')
  2530. self.assertEqual(self.transport.value(), b'0001 FETCH 1:7 (UID)\r\n')
  2531. self.client.lineReceived(b'* 2 FETCH (UID 22)')
  2532. self.client.lineReceived(b'* 3 FETCH (UID 23)')
  2533. self.client.lineReceived(b'* 4 FETCH (UID 24)')
  2534. self.client.lineReceived(b'* 5 FETCH (UID 25)')
  2535. self.client.lineReceived(b'0001 OK FETCH completed')
  2536. self.assertEqual(
  2537. self._extractDeferredResult(d), {
  2538. 2: {'UID': '22'},
  2539. 3: {'UID': '23'},
  2540. 4: {'UID': '24'},
  2541. 5: {'UID': '25'}})
  2542. def test_fetchUIDNonIntegerFound(self):
  2543. """
  2544. If the server responds with a non-integer where a message sequence
  2545. number is expected, the L{Deferred} returned by L{IMAP4Client.fetchUID}
  2546. fails with L{IllegalServerResponse}.
  2547. """
  2548. d = self.client.fetchUID(b'1')
  2549. self.assertEqual(self.transport.value(), b'0001 FETCH 1 (UID)\r\n')
  2550. self.client.lineReceived('* foo FETCH (UID 22)')
  2551. self.client.lineReceived('0001 OK FETCH completed')
  2552. self.assertRaises(
  2553. imap4.IllegalServerResponse, self._extractDeferredResult, d)
  2554. def test_incompleteFetchUIDResponse(self):
  2555. """
  2556. If the server responds with an incomplete I{FETCH} response line, the
  2557. L{Deferred} returned by L{IMAP4Client.fetchUID} fails with
  2558. L{IllegalServerResponse}.
  2559. """
  2560. d = self.client.fetchUID(b'1:7')
  2561. self.assertEqual(self.transport.value(), b'0001 FETCH 1:7 (UID)\r\n')
  2562. self.client.lineReceived(b'* 2 FETCH (UID 22)')
  2563. self.client.lineReceived(b'* 3 FETCH (UID)')
  2564. self.client.lineReceived(b'* 4 FETCH (UID 24)')
  2565. self.client.lineReceived(b'0001 OK FETCH completed')
  2566. self.assertRaises(
  2567. imap4.IllegalServerResponse, self._extractDeferredResult, d)
  2568. def test_fetchBody(self):
  2569. """
  2570. L{IMAP4Client.fetchBody} sends the I{FETCH BODY} command and returns a
  2571. L{Deferred} which fires with a C{dict} mapping message sequence numbers
  2572. to C{dict}s mapping C{'RFC822.TEXT'} to that message's body as given in
  2573. the server's response.
  2574. """
  2575. d = self.client.fetchBody(b'3')
  2576. self.assertEqual(
  2577. self.transport.value(), b'0001 FETCH 3 (RFC822.TEXT)\r\n')
  2578. self.client.lineReceived(b'* 3 FETCH (RFC822.TEXT "Message text")')
  2579. self.client.lineReceived(b'0001 OK FETCH completed')
  2580. self.assertEqual(
  2581. self._extractDeferredResult(d),
  2582. {3: {b'RFC822.TEXT': b'Message text'}})
  2583. def test_fetchSpecific(self):
  2584. """
  2585. L{IMAP4Client.fetchSpecific} sends the I{BODY[]} command if no
  2586. parameters beyond the message set to retrieve are given. It returns a
  2587. L{Deferred} which fires with a C{dict} mapping message sequence numbers
  2588. to C{list}s of corresponding message data given by the server's
  2589. response.
  2590. """
  2591. d = self.client.fetchSpecific(b'7')
  2592. self.assertEqual(
  2593. self.transport.value(), b'0001 FETCH 7 BODY[]\r\n')
  2594. self.client.lineReceived(b'* 7 FETCH (BODY[] "Some body")')
  2595. self.client.lineReceived(b'0001 OK FETCH completed')
  2596. self.assertEqual(
  2597. self._extractDeferredResult(d), {7: [['BODY', [], "Some body"]]})
  2598. def test_fetchSpecificPeek(self):
  2599. """
  2600. L{IMAP4Client.fetchSpecific} issues a I{BODY.PEEK[]} command if passed
  2601. C{True} for the C{peek} parameter.
  2602. """
  2603. d = self.client.fetchSpecific(b'6', peek=True)
  2604. self.assertEqual(
  2605. self.transport.value(), b'0001 FETCH 6 BODY.PEEK[]\r\n')
  2606. # BODY.PEEK responses are just BODY
  2607. self.client.lineReceived(b'* 6 FETCH (BODY[] "Some body")')
  2608. self.client.lineReceived(b'0001 OK FETCH completed')
  2609. self.assertEqual(
  2610. self._extractDeferredResult(d), {6: [['BODY', [], "Some body"]]})
  2611. def test_fetchSpecificNumbered(self):
  2612. """
  2613. L{IMAP4Client.fetchSpecific}, when passed a sequence for
  2614. C{headerNumber}, sends the I{BODY[N.M]} command. It returns a
  2615. L{Deferred} which fires with a C{dict} mapping message sequence numbers
  2616. to C{list}s of corresponding message data given by the server's
  2617. response.
  2618. """
  2619. d = self.client.fetchSpecific(b'7', headerNumber=(1, 2, 3))
  2620. self.assertEqual(
  2621. self.transport.value(), b'0001 FETCH 7 BODY[1.2.3]\r\n')
  2622. self.client.lineReceived(b'* 7 FETCH (BODY[1.2.3] "Some body")')
  2623. self.client.lineReceived(b'0001 OK FETCH completed')
  2624. self.assertEqual(
  2625. self._extractDeferredResult(d),
  2626. {7: [['BODY', ['1.2.3'], "Some body"]]})
  2627. def test_fetchSpecificText(self):
  2628. """
  2629. L{IMAP4Client.fetchSpecific}, when passed C{'TEXT'} for C{headerType},
  2630. sends the I{BODY[TEXT]} command. It returns a L{Deferred} which fires
  2631. with a C{dict} mapping message sequence numbers to C{list}s of
  2632. corresponding message data given by the server's response.
  2633. """
  2634. d = self.client.fetchSpecific(b'8', headerType=b'TEXT')
  2635. self.assertEqual(
  2636. self.transport.value(), b'0001 FETCH 8 BODY[TEXT]\r\n')
  2637. self.client.lineReceived(b'* 8 FETCH (BODY[TEXT] "Some body")')
  2638. self.client.lineReceived(b'0001 OK FETCH completed')
  2639. self.assertEqual(
  2640. self._extractDeferredResult(d),
  2641. {8: [[b'BODY', [b'TEXT'], b"Some body"]]})
  2642. def test_fetchSpecificNumberedText(self):
  2643. """
  2644. If passed a value for the C{headerNumber} parameter and C{'TEXT'} for
  2645. the C{headerType} parameter, L{IMAP4Client.fetchSpecific} sends a
  2646. I{BODY[number.TEXT]} request and returns a L{Deferred} which fires with
  2647. a C{dict} mapping message sequence numbers to C{list}s of message data
  2648. given by the server's response.
  2649. """
  2650. d = self.client.fetchSpecific(b'4', headerType=b'TEXT', headerNumber=7)
  2651. self.assertEqual(
  2652. self.transport.value(), b'0001 FETCH 4 BODY[7.TEXT]\r\n')
  2653. self.client.lineReceived(b'* 4 FETCH (BODY[7.TEXT] "Some body")')
  2654. self.client.lineReceived(b'0001 OK FETCH completed')
  2655. self.assertEqual(
  2656. self._extractDeferredResult(d),
  2657. {4: [['BODY', ['7.TEXT'], "Some body"]]})
  2658. def test_incompleteFetchSpecificTextResponse(self):
  2659. """
  2660. If the server responds to a I{BODY[TEXT]} request with a I{FETCH} line
  2661. which is truncated after the I{BODY[TEXT]} tokens, the L{Deferred}
  2662. returned by L{IMAP4Client.fetchUID} fails with
  2663. L{IllegalServerResponse}.
  2664. """
  2665. d = self.client.fetchSpecific(b'8', headerType=b'TEXT')
  2666. self.assertEqual(
  2667. self.transport.value(), b'0001 FETCH 8 BODY[TEXT]\r\n')
  2668. self.client.lineReceived(b'* 8 FETCH (BODY[TEXT])')
  2669. self.client.lineReceived(b'0001 OK FETCH completed')
  2670. self.assertRaises(
  2671. imap4.IllegalServerResponse, self._extractDeferredResult, d)
  2672. def test_fetchSpecificMIME(self):
  2673. """
  2674. L{IMAP4Client.fetchSpecific}, when passed C{'MIME'} for C{headerType},
  2675. sends the I{BODY[MIME]} command. It returns a L{Deferred} which fires
  2676. with a C{dict} mapping message sequence numbers to C{list}s of
  2677. corresponding message data given by the server's response.
  2678. """
  2679. d = self.client.fetchSpecific(b'8', headerType=b'MIME')
  2680. self.assertEqual(
  2681. self.transport.value(), b'0001 FETCH 8 BODY[MIME]\r\n')
  2682. self.client.lineReceived(b'* 8 FETCH (BODY[MIME] "Some body")')
  2683. self.client.lineReceived(b'0001 OK FETCH completed')
  2684. self.assertEqual(
  2685. self._extractDeferredResult(d),
  2686. {8: [['BODY', ['MIME'], "Some body"]]})
  2687. def test_fetchSpecificPartial(self):
  2688. """
  2689. L{IMAP4Client.fetchSpecific}, when passed C{offset} and C{length},
  2690. sends a partial content request (like I{BODY[TEXT]<offset.length>}).
  2691. It returns a L{Deferred} which fires with a C{dict} mapping message
  2692. sequence numbers to C{list}s of corresponding message data given by the
  2693. server's response.
  2694. """
  2695. d = self.client.fetchSpecific(
  2696. b'9', headerType=b'TEXT', offset=17, length=3)
  2697. self.assertEqual(
  2698. self.transport.value(), b'0001 FETCH 9 BODY[TEXT]<17.3>\r\n')
  2699. self.client.lineReceived(b'* 9 FETCH (BODY[TEXT]<17> "foo")')
  2700. self.client.lineReceived(b'0001 OK FETCH completed')
  2701. self.assertEqual(
  2702. self._extractDeferredResult(d),
  2703. {9: [[b'BODY', [b'TEXT'], b'<17>', b'foo']]})
  2704. def test_incompleteFetchSpecificPartialResponse(self):
  2705. """
  2706. If the server responds to a I{BODY[TEXT]} request with a I{FETCH} line
  2707. which is truncated after the I{BODY[TEXT]<offset>} tokens, the
  2708. L{Deferred} returned by L{IMAP4Client.fetchUID} fails with
  2709. L{IllegalServerResponse}.
  2710. """
  2711. d = self.client.fetchSpecific(b'8', headerType=b'TEXT')
  2712. self.assertEqual(
  2713. self.transport.value(), b'0001 FETCH 8 BODY[TEXT]\r\n')
  2714. self.client.lineReceived(b'* 8 FETCH (BODY[TEXT]<17>)')
  2715. self.client.lineReceived(b'0001 OK FETCH completed')
  2716. self.assertRaises(
  2717. imap4.IllegalServerResponse, self._extractDeferredResult, d)
  2718. def test_fetchSpecificHTML(self):
  2719. """
  2720. If the body of a message begins with I{<} and ends with I{>} (as,
  2721. for example, HTML bodies typically will), this is still interpreted
  2722. as the body by L{IMAP4Client.fetchSpecific} (and particularly, not
  2723. as a length indicator for a response to a request for a partial
  2724. body).
  2725. """
  2726. d = self.client.fetchSpecific(b'7')
  2727. self.assertEqual(
  2728. self.transport.value(), b'0001 FETCH 7 BODY[]\r\n')
  2729. self.client.lineReceived(b'* 7 FETCH (BODY[] "<html>test</html>")')
  2730. self.client.lineReceived(b'0001 OK FETCH completed')
  2731. self.assertEqual(
  2732. self._extractDeferredResult(d), {7: [[b'BODY', [], b"<html>test</html>"]]})
  2733. class IMAP4ClientStoreTests(PreauthIMAP4ClientMixin, unittest.TestCase):
  2734. """
  2735. Tests for the L{IMAP4Client.setFlags}, L{IMAP4Client.addFlags}, and
  2736. L{IMAP4Client.removeFlags} methods.
  2737. An example of usage of the STORE command, in terms of which these three
  2738. methods are implemented, from RFC 3501, section 6.4.6::
  2739. C: A003 STORE 2:4 +FLAGS (\Deleted)
  2740. S: * 2 FETCH (FLAGS (\Deleted \Seen))
  2741. S: * 3 FETCH (FLAGS (\Deleted))
  2742. S: * 4 FETCH (FLAGS (\Deleted \Flagged \Seen))
  2743. S: A003 OK STORE completed
  2744. """
  2745. clientProtocol = StillSimplerClient
  2746. def _flagsTest(self, method, item):
  2747. """
  2748. Test a non-silent flag modifying method. Call the method, assert that
  2749. the correct bytes are sent, deliver a I{FETCH} response, and assert
  2750. that the result of the Deferred returned by the method is correct.
  2751. @param method: The name of the method to test.
  2752. @param item: The data item which is expected to be specified.
  2753. """
  2754. d = getattr(self.client, method)(b'3', (b'\\Read', b'\\Seen'), False)
  2755. self.assertEqual(
  2756. self.transport.value(),
  2757. b'0001 STORE 3 ' + item + b' (\\Read \\Seen)\r\n')
  2758. self.client.lineReceived(b'* 3 FETCH (FLAGS (\\Read \\Seen))')
  2759. self.client.lineReceived(b'0001 OK STORE completed')
  2760. self.assertEqual(
  2761. self._extractDeferredResult(d),
  2762. {3: {b'FLAGS': [b'\\Read', b'\\Seen']}})
  2763. def _flagsSilentlyTest(self, method, item):
  2764. """
  2765. Test a silent flag modifying method. Call the method, assert that the
  2766. correct bytes are sent, deliver an I{OK} response, and assert that the
  2767. result of the Deferred returned by the method is correct.
  2768. @param method: The name of the method to test.
  2769. @param item: The data item which is expected to be specified.
  2770. """
  2771. d = getattr(self.client, method)(b'3', (b'\\Read', b'\\Seen'), True)
  2772. self.assertEqual(
  2773. self.transport.value(),
  2774. b'0001 STORE 3 ' + item + b' (\\Read \\Seen)\r\n')
  2775. self.client.lineReceived(b'0001 OK STORE completed')
  2776. self.assertEqual(self._extractDeferredResult(d), {})
  2777. def _flagsSilentlyWithUnsolicitedDataTest(self, method, item):
  2778. """
  2779. Test unsolicited data received in response to a silent flag modifying
  2780. method. Call the method, assert that the correct bytes are sent,
  2781. deliver the unsolicited I{FETCH} response, and assert that the result
  2782. of the Deferred returned by the method is correct.
  2783. @param method: The name of the method to test.
  2784. @param item: The data item which is expected to be specified.
  2785. """
  2786. d = getattr(self.client, method)(b'3', (b'\\Read', b'\\Seen'), True)
  2787. self.assertEqual(
  2788. self.transport.value(),
  2789. b'0001 STORE 3 ' + item + b' (\\Read \\Seen)\r\n')
  2790. self.client.lineReceived(b'* 2 FETCH (FLAGS (\\Read \\Seen))')
  2791. self.client.lineReceived(b'0001 OK STORE completed')
  2792. self.assertEqual(self._extractDeferredResult(d), {})
  2793. self.assertEqual(self.client.flags, {2: [b'\\Read', b'\\Seen']})
  2794. def test_setFlags(self):
  2795. """
  2796. When passed a C{False} value for the C{silent} parameter,
  2797. L{IMAP4Client.setFlags} sends the I{STORE} command with a I{FLAGS} data
  2798. item and returns a L{Deferred} which fires with a C{dict} mapping
  2799. message sequence numbers to C{dict}s mapping C{'FLAGS'} to the new
  2800. flags of those messages.
  2801. """
  2802. self._flagsTest('setFlags', b'FLAGS')
  2803. def test_setFlagsSilently(self):
  2804. """
  2805. When passed a C{True} value for the C{silent} parameter,
  2806. L{IMAP4Client.setFlags} sends the I{STORE} command with a
  2807. I{FLAGS.SILENT} data item and returns a L{Deferred} which fires with an
  2808. empty dictionary.
  2809. """
  2810. self._flagsSilentlyTest('setFlags', b'FLAGS.SILENT')
  2811. def test_setFlagsSilentlyWithUnsolicitedData(self):
  2812. """
  2813. If unsolicited flag data is received in response to a I{STORE}
  2814. I{FLAGS.SILENT} request, that data is passed to the C{flagsChanged}
  2815. callback.
  2816. """
  2817. self._flagsSilentlyWithUnsolicitedDataTest('setFlags', b'FLAGS.SILENT')
  2818. def test_addFlags(self):
  2819. """
  2820. L{IMAP4Client.addFlags} is like L{IMAP4Client.setFlags}, but sends
  2821. I{+FLAGS} instead of I{FLAGS}.
  2822. """
  2823. self._flagsTest('addFlags', b'+FLAGS')
  2824. def test_addFlagsSilently(self):
  2825. """
  2826. L{IMAP4Client.addFlags} with a C{True} value for C{silent} behaves like
  2827. L{IMAP4Client.setFlags} with a C{True} value for C{silent}, but it
  2828. sends I{+FLAGS.SILENT} instead of I{FLAGS.SILENT}.
  2829. """
  2830. self._flagsSilentlyTest('addFlags', b'+FLAGS.SILENT')
  2831. def test_addFlagsSilentlyWithUnsolicitedData(self):
  2832. """
  2833. L{IMAP4Client.addFlags} behaves like L{IMAP4Client.setFlags} when used
  2834. in silent mode and unsolicited data is received.
  2835. """
  2836. self._flagsSilentlyWithUnsolicitedDataTest('addFlags', b'+FLAGS.SILENT')
  2837. def test_removeFlags(self):
  2838. """
  2839. L{IMAP4Client.removeFlags} is like L{IMAP4Client.setFlags}, but sends
  2840. I{-FLAGS} instead of I{FLAGS}.
  2841. """
  2842. self._flagsTest('removeFlags', b'-FLAGS')
  2843. def test_removeFlagsSilently(self):
  2844. """
  2845. L{IMAP4Client.removeFlags} with a C{True} value for C{silent} behaves
  2846. like L{IMAP4Client.setFlags} with a C{True} value for C{silent}, but it
  2847. sends I{-FLAGS.SILENT} instead of I{FLAGS.SILENT}.
  2848. """
  2849. self._flagsSilentlyTest('removeFlags', b'-FLAGS.SILENT')
  2850. def test_removeFlagsSilentlyWithUnsolicitedData(self):
  2851. """
  2852. L{IMAP4Client.removeFlags} behaves like L{IMAP4Client.setFlags} when
  2853. used in silent mode and unsolicited data is received.
  2854. """
  2855. self._flagsSilentlyWithUnsolicitedDataTest('removeFlags', b'-FLAGS.SILENT')
  2856. class FakeyServer(imap4.IMAP4Server):
  2857. state = 'select'
  2858. timeout = None
  2859. def sendServerGreeting(self):
  2860. pass
  2861. @implementer(imap4.IMessage)
  2862. class FakeyMessage(util.FancyStrMixin):
  2863. showAttributes = ('headers', 'flags', 'date', '_body', 'uid')
  2864. def __init__(self, headers, flags, date, body, uid, subpart):
  2865. self.headers = headers
  2866. self.flags = flags
  2867. self._body = body
  2868. self.size = len(body)
  2869. self.date = date
  2870. self.uid = uid
  2871. self.subpart = subpart
  2872. def getHeaders(self, negate, *names):
  2873. self.got_headers = negate, names
  2874. return self.headers
  2875. def getFlags(self):
  2876. return self.flags
  2877. def getInternalDate(self):
  2878. return self.date
  2879. def getBodyFile(self):
  2880. return BytesIO(self._body)
  2881. def getSize(self):
  2882. return self.size
  2883. def getUID(self):
  2884. return self.uid
  2885. def isMultipart(self):
  2886. return self.subpart is not None
  2887. def getSubPart(self, part):
  2888. self.got_subpart = part
  2889. return self.subpart[part]
  2890. class NewStoreTests(unittest.TestCase, IMAP4HelperMixin):
  2891. result = None
  2892. storeArgs = None
  2893. def setUp(self):
  2894. self.received_messages = self.received_uid = None
  2895. self.server = imap4.IMAP4Server()
  2896. self.server.state = 'select'
  2897. self.server.mbox = self
  2898. self.connected = defer.Deferred()
  2899. self.client = SimpleClient(self.connected)
  2900. def addListener(self, x):
  2901. pass
  2902. def removeListener(self, x):
  2903. pass
  2904. def store(self, *args, **kw):
  2905. self.storeArgs = args, kw
  2906. return self.response
  2907. def _storeWork(self):
  2908. def connected():
  2909. return self.function(self.messages, self.flags, self.silent, self.uid)
  2910. def result(R):
  2911. self.result = R
  2912. self.connected.addCallback(strip(connected)
  2913. ).addCallback(result
  2914. ).addCallback(self._cbStopClient
  2915. ).addErrback(self._ebGeneral)
  2916. def check(ignored):
  2917. self.assertEqual(self.result, self.expected)
  2918. self.assertEqual(self.storeArgs, self.expectedArgs)
  2919. d = loopback.loopbackTCP(self.server, self.client, noisy=False)
  2920. d.addCallback(check)
  2921. return d
  2922. def testSetFlags(self, uid=0):
  2923. self.function = self.client.setFlags
  2924. self.messages = b'1,5,9'
  2925. self.flags = [b'\\A', b'\\B', b'C']
  2926. self.silent = False
  2927. self.uid = uid
  2928. self.response = {
  2929. 1: [b'\\A', b'\\B', b'C'],
  2930. 5: [b'\\A', b'\\B', b'C'],
  2931. 9: [b'\\A', b'\\B', b'C'],
  2932. }
  2933. self.expected = {
  2934. 1: {b'FLAGS': [b'\\A', b'\\B', b'C']},
  2935. 5: {b'FLAGS': [b'\\A', b'\\B', b'C']},
  2936. 9: {b'FLAGS': [b'\\A', b'\\B', b'C']},
  2937. }
  2938. msg = imap4.MessageSet()
  2939. msg.add(1)
  2940. msg.add(5)
  2941. msg.add(9)
  2942. self.expectedArgs = ((msg, [b'\\A', b'\\B', b'C'], 0), {b'uid': 0})
  2943. return self._storeWork()
  2944. class GetBodyStructureTests(unittest.TestCase):
  2945. """
  2946. Tests for L{imap4.getBodyStructure}, a helper for constructing a list which
  2947. directly corresponds to the wire information needed for a I{BODY} or
  2948. I{BODYSTRUCTURE} response.
  2949. """
  2950. def test_singlePart(self):
  2951. """
  2952. L{imap4.getBodyStructure} accepts a L{IMessagePart} provider and returns
  2953. a list giving the basic fields for the I{BODY} response for that
  2954. message.
  2955. """
  2956. body = b'hello, world'
  2957. major = 'image'
  2958. minor = 'jpeg'
  2959. charset = 'us-ascii'
  2960. identifier = 'some kind of id'
  2961. description = 'great justice'
  2962. encoding = 'maximum'
  2963. msg = FakeyMessage({
  2964. 'content-type': major + '/' + minor +
  2965. '; charset=' + charset + '; x=y',
  2966. 'content-id': identifier,
  2967. 'content-description': description,
  2968. 'content-transfer-encoding': encoding,
  2969. }, (), b'', body, 123, None)
  2970. structure = imap4.getBodyStructure(msg)
  2971. self.assertEqual(
  2972. [major, minor, ["charset", charset, 'x', 'y'], identifier,
  2973. description, encoding, len(body)],
  2974. structure)
  2975. def test_singlePartExtended(self):
  2976. """
  2977. L{imap4.getBodyStructure} returns a list giving the basic and extended
  2978. fields for a I{BODYSTRUCTURE} response if passed C{True} for the
  2979. C{extended} parameter.
  2980. """
  2981. body = b'hello, world'
  2982. major = 'image'
  2983. minor = 'jpeg'
  2984. charset = 'us-ascii'
  2985. identifier = 'some kind of id'
  2986. description = 'great justice'
  2987. encoding = 'maximum'
  2988. md5 = 'abcdefabcdef'
  2989. msg = FakeyMessage({
  2990. 'content-type': major + '/' + minor +
  2991. '; charset=' + charset + '; x=y',
  2992. 'content-id': identifier,
  2993. 'content-description': description,
  2994. 'content-transfer-encoding': encoding,
  2995. 'content-md5': md5,
  2996. 'content-disposition': 'attachment; name=foo; size=bar',
  2997. 'content-language': 'fr',
  2998. 'content-location': 'France',
  2999. }, (), '', body, 123, None)
  3000. structure = imap4.getBodyStructure(msg, extended=True)
  3001. self.assertEqual(
  3002. [major, minor, ["charset", charset, 'x', 'y'], identifier,
  3003. description, encoding, len(body), md5,
  3004. ['attachment', ['name', 'foo', 'size', 'bar']], 'fr', 'France'],
  3005. structure)
  3006. def test_singlePartWithMissing(self):
  3007. """
  3008. For fields with no information contained in the message headers,
  3009. L{imap4.getBodyStructure} fills in L{None} values in its result.
  3010. """
  3011. major = 'image'
  3012. minor = 'jpeg'
  3013. body = b'hello, world'
  3014. msg = FakeyMessage({
  3015. 'content-type': major + '/' + minor
  3016. }, (), b'', body, 123, None)
  3017. structure = imap4.getBodyStructure(msg, extended=True)
  3018. self.assertEqual(
  3019. [major, minor, None, None, None, None, len(body), None, None,
  3020. None, None],
  3021. structure)
  3022. def test_textPart(self):
  3023. """
  3024. For a I{text/*} message, the number of lines in the message body are
  3025. included after the common single-part basic fields.
  3026. """
  3027. body = b'hello, world\nhow are you?\ngoodbye\n'
  3028. major = 'text'
  3029. minor = 'jpeg'
  3030. charset = 'us-ascii'
  3031. identifier = 'some kind of id'
  3032. description = 'great justice'
  3033. encoding = 'maximum'
  3034. msg = FakeyMessage({
  3035. 'content-type': major + '/' + minor +
  3036. '; charset=' + charset + '; x=y',
  3037. 'content-id': identifier,
  3038. 'content-description': description,
  3039. 'content-transfer-encoding': encoding,
  3040. }, (), b'', body, 123, None)
  3041. structure = imap4.getBodyStructure(msg)
  3042. self.assertEqual(
  3043. [major, minor, ["charset", charset, 'x', 'y'], identifier,
  3044. description, encoding, len(body), len(body.splitlines())],
  3045. structure)
  3046. def test_rfc822Message(self):
  3047. """
  3048. For a I{message/rfc822} message, the common basic fields are followed
  3049. by information about the contained message.
  3050. """
  3051. body = b'hello, world\nhow are you?\ngoodbye\n'
  3052. major = 'text'
  3053. minor = 'jpeg'
  3054. charset = 'us-ascii'
  3055. identifier = 'some kind of id'
  3056. description = 'great justice'
  3057. encoding = 'maximum'
  3058. msg = FakeyMessage({
  3059. 'content-type': major + '/' + minor +
  3060. '; charset=' + charset + '; x=y',
  3061. 'from': 'Alice <alice@example.com>',
  3062. 'to': 'Bob <bob@example.com>',
  3063. 'content-id': identifier,
  3064. 'content-description': description,
  3065. 'content-transfer-encoding': encoding,
  3066. }, (), '', body, 123, None)
  3067. container = FakeyMessage({
  3068. 'content-type': 'message/rfc822',
  3069. }, (), b'', b'', 123, [msg])
  3070. structure = imap4.getBodyStructure(container)
  3071. self.assertEqual(
  3072. ['message', 'rfc822', None, None, None, None, 0,
  3073. imap4.getEnvelope(msg), imap4.getBodyStructure(msg), 3],
  3074. structure)
  3075. def test_multiPart(self):
  3076. """
  3077. For a I{multipart/*} message, L{imap4.getBodyStructure} returns a list
  3078. containing the body structure information for each part of the message
  3079. followed by an element giving the MIME subtype of the message.
  3080. """
  3081. oneSubPart = FakeyMessage({
  3082. 'content-type': 'image/jpeg; x=y',
  3083. 'content-id': 'some kind of id',
  3084. 'content-description': 'great justice',
  3085. 'content-transfer-encoding': 'maximum',
  3086. }, (), b'', b'hello world', 123, None)
  3087. anotherSubPart = FakeyMessage({
  3088. 'content-type': 'text/plain; charset=us-ascii',
  3089. }, (), b'', b'some stuff', 321, None)
  3090. container = FakeyMessage({
  3091. 'content-type': 'multipart/related',
  3092. }, (), b'', b'', 555, [oneSubPart, anotherSubPart])
  3093. self.assertEqual(
  3094. [imap4.getBodyStructure(oneSubPart),
  3095. imap4.getBodyStructure(anotherSubPart),
  3096. 'related'],
  3097. imap4.getBodyStructure(container))
  3098. def test_multiPartExtended(self):
  3099. """
  3100. When passed a I{multipart/*} message and C{True} for the C{extended}
  3101. argument, L{imap4.getBodyStructure} includes extended structure
  3102. information from the parts of the multipart message and extended
  3103. structure information about the multipart message itself.
  3104. """
  3105. oneSubPart = FakeyMessage({
  3106. b'content-type': b'image/jpeg; x=y',
  3107. b'content-id': b'some kind of id',
  3108. b'content-description': b'great justice',
  3109. b'content-transfer-encoding': b'maximum',
  3110. }, (), b'', b'hello world', 123, None)
  3111. anotherSubPart = FakeyMessage({
  3112. b'content-type': b'text/plain; charset=us-ascii',
  3113. }, (), b'', b'some stuff', 321, None)
  3114. container = FakeyMessage({
  3115. 'content-type': 'multipart/related; foo=bar',
  3116. 'content-language': 'es',
  3117. 'content-location': 'Spain',
  3118. 'content-disposition': 'attachment; name=monkeys',
  3119. }, (), b'', b'', 555, [oneSubPart, anotherSubPart])
  3120. self.assertEqual(
  3121. [imap4.getBodyStructure(oneSubPart, extended=True),
  3122. imap4.getBodyStructure(anotherSubPart, extended=True),
  3123. 'related', ['foo', 'bar'], ['attachment', ['name', 'monkeys']],
  3124. 'es', 'Spain'],
  3125. imap4.getBodyStructure(container, extended=True))
  3126. class NewFetchTests(unittest.TestCase, IMAP4HelperMixin):
  3127. def setUp(self):
  3128. self.received_messages = self.received_uid = None
  3129. self.result = None
  3130. self.server = imap4.IMAP4Server()
  3131. self.server.state = 'select'
  3132. self.server.mbox = self
  3133. self.connected = defer.Deferred()
  3134. self.client = SimpleClient(self.connected)
  3135. def addListener(self, x):
  3136. pass
  3137. def removeListener(self, x):
  3138. pass
  3139. def fetch(self, messages, uid):
  3140. self.received_messages = messages
  3141. self.received_uid = uid
  3142. return iter(zip(range(len(self.msgObjs)), self.msgObjs))
  3143. def _fetchWork(self, uid):
  3144. if uid:
  3145. for (i, msg) in zip(range(len(self.msgObjs)), self.msgObjs):
  3146. self.expected[i]['UID'] = str(msg.getUID())
  3147. def result(R):
  3148. self.result = R
  3149. self.connected.addCallback(lambda _: self.function(self.messages, uid)
  3150. ).addCallback(result
  3151. ).addCallback(self._cbStopClient
  3152. ).addErrback(self._ebGeneral)
  3153. d = loopback.loopbackTCP(self.server, self.client, noisy=False)
  3154. d.addCallback(lambda x : self.assertEqual(self.result, self.expected))
  3155. return d
  3156. def testFetchUID(self):
  3157. self.function = lambda m, u: self.client.fetchUID(m)
  3158. self.messages = '7'
  3159. self.msgObjs = [
  3160. FakeyMessage({}, (), '', '', 12345, None),
  3161. FakeyMessage({}, (), '', '', 999, None),
  3162. FakeyMessage({}, (), '', '', 10101, None),
  3163. ]
  3164. self.expected = {
  3165. 0: {'UID': '12345'},
  3166. 1: {'UID': '999'},
  3167. 2: {'UID': '10101'},
  3168. }
  3169. return self._fetchWork(0)
  3170. def testFetchFlags(self, uid=0):
  3171. self.function = self.client.fetchFlags
  3172. self.messages = '9'
  3173. self.msgObjs = [
  3174. FakeyMessage({}, ['FlagA', 'FlagB', '\\FlagC'], '', '', 54321, None),
  3175. FakeyMessage({}, ['\\FlagC', 'FlagA', 'FlagB'], '', '', 12345, None),
  3176. ]
  3177. self.expected = {
  3178. 0: {'FLAGS': ['FlagA', 'FlagB', '\\FlagC']},
  3179. 1: {'FLAGS': ['\\FlagC', 'FlagA', 'FlagB']},
  3180. }
  3181. return self._fetchWork(uid)
  3182. def testFetchFlagsUID(self):
  3183. return self.testFetchFlags(1)
  3184. def testFetchInternalDate(self, uid=0):
  3185. self.function = self.client.fetchInternalDate
  3186. self.messages = '13'
  3187. self.msgObjs = [
  3188. FakeyMessage({}, (), 'Fri, 02 Nov 2003 21:25:10 GMT', '', 23232, None),
  3189. FakeyMessage({}, (), 'Thu, 29 Dec 2013 11:31:52 EST', '', 101, None),
  3190. FakeyMessage({}, (), 'Mon, 10 Mar 1992 02:44:30 CST', '', 202, None),
  3191. FakeyMessage({}, (), 'Sat, 11 Jan 2000 14:40:24 PST', '', 303, None),
  3192. ]
  3193. self.expected = {
  3194. 0: {'INTERNALDATE': '02-Nov-2003 21:25:10 +0000'},
  3195. 1: {'INTERNALDATE': '29-Dec-2013 11:31:52 -0500'},
  3196. 2: {'INTERNALDATE': '10-Mar-1992 02:44:30 -0600'},
  3197. 3: {'INTERNALDATE': '11-Jan-2000 14:40:24 -0800'},
  3198. }
  3199. return self._fetchWork(uid)
  3200. def testFetchInternalDateUID(self):
  3201. return self.testFetchInternalDate(1)
  3202. def test_fetchInternalDateLocaleIndependent(self):
  3203. """
  3204. The month name in the date is locale independent.
  3205. """
  3206. # Fake that we're in a language where December is not Dec
  3207. currentLocale = locale.setlocale(locale.LC_ALL, None)
  3208. locale.setlocale(locale.LC_ALL, "es_AR.UTF8")
  3209. self.addCleanup(locale.setlocale, locale.LC_ALL, currentLocale)
  3210. return self.testFetchInternalDate(1)
  3211. # if alternate locale is not available, the previous test will be skipped,
  3212. # please install this locale for it to run. Avoid using locale.getlocale to
  3213. # learn the current locale; its values don't round-trip well on all
  3214. # platforms. Fortunately setlocale returns a value which does round-trip
  3215. # well.
  3216. currentLocale = locale.setlocale(locale.LC_ALL, None)
  3217. try:
  3218. locale.setlocale(locale.LC_ALL, "es_AR.UTF8")
  3219. except locale.Error:
  3220. test_fetchInternalDateLocaleIndependent.skip = (
  3221. "The es_AR.UTF8 locale is not installed.")
  3222. else:
  3223. locale.setlocale(locale.LC_ALL, currentLocale)
  3224. def testFetchEnvelope(self, uid=0):
  3225. self.function = self.client.fetchEnvelope
  3226. self.messages = '15'
  3227. self.msgObjs = [
  3228. FakeyMessage({
  3229. 'from': 'user@domain', 'to': 'resu@domain',
  3230. 'date': 'thursday', 'subject': 'it is a message',
  3231. 'message-id': 'id-id-id-yayaya'}, (), '', '', 65656,
  3232. None),
  3233. ]
  3234. self.expected = {
  3235. 0: {'ENVELOPE':
  3236. ['thursday', 'it is a message',
  3237. [[None, None, 'user', 'domain']],
  3238. [[None, None, 'user', 'domain']],
  3239. [[None, None, 'user', 'domain']],
  3240. [[None, None, 'resu', 'domain']],
  3241. None, None, None, 'id-id-id-yayaya']
  3242. }
  3243. }
  3244. return self._fetchWork(uid)
  3245. def testFetchEnvelopeUID(self):
  3246. return self.testFetchEnvelope(1)
  3247. def test_fetchBodyStructure(self, uid=0):
  3248. """
  3249. L{IMAP4Client.fetchBodyStructure} issues a I{FETCH BODYSTRUCTURE}
  3250. command and returns a Deferred which fires with a structure giving the
  3251. result of parsing the server's response. The structure is a list
  3252. reflecting the parenthesized data sent by the server, as described by
  3253. RFC 3501, section 7.4.2.
  3254. """
  3255. self.function = self.client.fetchBodyStructure
  3256. self.messages = b'3:9,10:*'
  3257. self.msgObjs = [FakeyMessage({
  3258. 'content-type': 'text/plain; name=thing; key="value"',
  3259. 'content-id': 'this-is-the-content-id',
  3260. 'content-description': 'describing-the-content-goes-here!',
  3261. 'content-transfer-encoding': '8BIT',
  3262. 'content-md5': 'abcdef123456',
  3263. 'content-disposition': 'attachment; filename=monkeys',
  3264. 'content-language': 'es',
  3265. 'content-location': 'http://example.com/monkeys',
  3266. }, (), '', b'Body\nText\nGoes\nHere\n', 919293, None)]
  3267. self.expected = {0: {'BODYSTRUCTURE': [
  3268. 'text', 'plain', ['key', 'value', 'name', 'thing'],
  3269. 'this-is-the-content-id', 'describing-the-content-goes-here!',
  3270. '8BIT', '20', '4', 'abcdef123456',
  3271. ['attachment', ['filename', 'monkeys']], 'es',
  3272. 'http://example.com/monkeys']}}
  3273. return self._fetchWork(uid)
  3274. def testFetchBodyStructureUID(self):
  3275. """
  3276. If passed C{True} for the C{uid} argument, C{fetchBodyStructure} can
  3277. also issue a I{UID FETCH BODYSTRUCTURE} command.
  3278. """
  3279. return self.test_fetchBodyStructure(1)
  3280. def test_fetchBodyStructureMultipart(self, uid=0):
  3281. """
  3282. L{IMAP4Client.fetchBodyStructure} can also parse the response to a
  3283. I{FETCH BODYSTRUCTURE} command for a multipart message.
  3284. """
  3285. self.function = self.client.fetchBodyStructure
  3286. self.messages = '3:9,10:*'
  3287. innerMessage = FakeyMessage({
  3288. 'content-type': 'text/plain; name=thing; key="value"',
  3289. 'content-id': 'this-is-the-content-id',
  3290. 'content-description': 'describing-the-content-goes-here!',
  3291. 'content-transfer-encoding': '8BIT',
  3292. 'content-language': 'fr',
  3293. 'content-md5': '123456abcdef',
  3294. 'content-disposition': 'inline',
  3295. 'content-location': 'outer space',
  3296. }, (), '', 'Body\nText\nGoes\nHere\n', 919293, None)
  3297. self.msgObjs = [FakeyMessage({
  3298. 'content-type': 'multipart/mixed; boundary="xyz"',
  3299. 'content-language': 'en',
  3300. 'content-location': 'nearby',
  3301. }, (), '', '', 919293, [innerMessage])]
  3302. self.expected = {0: {'BODYSTRUCTURE': [
  3303. ['text', 'plain', ['key', 'value', 'name', 'thing'],
  3304. 'this-is-the-content-id', 'describing-the-content-goes-here!',
  3305. '8BIT', '20', '4', '123456abcdef', ['inline', None], 'fr',
  3306. 'outer space'],
  3307. 'mixed', ['boundary', 'xyz'], None, 'en', 'nearby'
  3308. ]}}
  3309. return self._fetchWork(uid)
  3310. def testFetchSimplifiedBody(self, uid=0):
  3311. self.function = self.client.fetchSimplifiedBody
  3312. self.messages = '21'
  3313. self.msgObjs = [FakeyMessage({}, (), '', 'Yea whatever', 91825,
  3314. [FakeyMessage({'content-type': 'image/jpg'}, (), '',
  3315. 'Body Body Body', None, None
  3316. )]
  3317. )]
  3318. self.expected = {0:
  3319. {'BODY':
  3320. [None, None, None, None, None, None,
  3321. '12'
  3322. ]
  3323. }
  3324. }
  3325. return self._fetchWork(uid)
  3326. def testFetchSimplifiedBodyUID(self):
  3327. return self.testFetchSimplifiedBody(1)
  3328. def testFetchSimplifiedBodyText(self, uid=0):
  3329. self.function = self.client.fetchSimplifiedBody
  3330. self.messages = '21'
  3331. self.msgObjs = [FakeyMessage({'content-type': 'text/plain'},
  3332. (), '', 'Yea whatever', 91825, None)]
  3333. self.expected = {0:
  3334. {'BODY':
  3335. ['text', 'plain', None, None, None, None,
  3336. '12', '1'
  3337. ]
  3338. }
  3339. }
  3340. return self._fetchWork(uid)
  3341. def testFetchSimplifiedBodyTextUID(self):
  3342. return self.testFetchSimplifiedBodyText(1)
  3343. def testFetchSimplifiedBodyRFC822(self, uid=0):
  3344. self.function = self.client.fetchSimplifiedBody
  3345. self.messages = '21'
  3346. self.msgObjs = [FakeyMessage({'content-type': 'message/rfc822'},
  3347. (), '', 'Yea whatever', 91825,
  3348. [FakeyMessage({'content-type': 'image/jpg'}, (), '',
  3349. 'Body Body Body', None, None
  3350. )]
  3351. )]
  3352. self.expected = {0:
  3353. {'BODY':
  3354. ['message', 'rfc822', None, None, None, None,
  3355. '12', [None, None, [[None, None, None]],
  3356. [[None, None, None]], None, None, None,
  3357. None, None, None], ['image', 'jpg', None,
  3358. None, None, None, '14'], '1'
  3359. ]
  3360. }
  3361. }
  3362. return self._fetchWork(uid)
  3363. def testFetchSimplifiedBodyRFC822UID(self):
  3364. return self.testFetchSimplifiedBodyRFC822(1)
  3365. def test_fetchSimplifiedBodyMultipart(self):
  3366. """
  3367. L{IMAP4Client.fetchSimplifiedBody} returns a dictionary mapping message
  3368. sequence numbers to fetch responses for the corresponding messages. In
  3369. particular, for a multipart message, the value in the dictionary maps
  3370. the string C{"BODY"} to a list giving the body structure information for
  3371. that message, in the form of a list of subpart body structure
  3372. information followed by the subtype of the message (eg C{"alternative"}
  3373. for a I{multipart/alternative} message). This structure is self-similar
  3374. in the case where a subpart is itself multipart.
  3375. """
  3376. self.function = self.client.fetchSimplifiedBody
  3377. self.messages = '21'
  3378. # A couple non-multipart messages to use as the inner-most payload
  3379. singles = [
  3380. FakeyMessage(
  3381. {'content-type': 'text/plain'},
  3382. (), 'date', 'Stuff', 54321, None),
  3383. FakeyMessage(
  3384. {'content-type': 'text/html'},
  3385. (), 'date', 'Things', 32415, None)]
  3386. # A multipart/alternative message containing the above non-multipart
  3387. # messages. This will be the payload of the outer-most message.
  3388. alternative = FakeyMessage(
  3389. {'content-type': 'multipart/alternative'},
  3390. (), '', 'Irrelevant', 12345, singles)
  3391. # The outer-most message, also with a multipart type, containing just
  3392. # the single middle message.
  3393. mixed = FakeyMessage(
  3394. # The message is multipart/mixed
  3395. {'content-type': 'multipart/mixed'},
  3396. (), '', 'RootOf', 98765, [alternative])
  3397. self.msgObjs = [mixed]
  3398. self.expected = {
  3399. 0: {'BODY': [
  3400. [['text', 'plain', None, None, None, None, '5', '1'],
  3401. ['text', 'html', None, None, None, None, '6', '1'],
  3402. 'alternative'],
  3403. 'mixed']}}
  3404. return self._fetchWork(False)
  3405. def testFetchMessage(self, uid=0):
  3406. self.function = self.client.fetchMessage
  3407. self.messages = '1,3,7,10101'
  3408. self.msgObjs = [
  3409. FakeyMessage({'Header': 'Value'}, (), '', 'BODY TEXT\r\n', 91, None),
  3410. ]
  3411. self.expected = {
  3412. 0: {'RFC822': 'Header: Value\r\n\r\nBODY TEXT\r\n'}
  3413. }
  3414. return self._fetchWork(uid)
  3415. def testFetchMessageUID(self):
  3416. return self.testFetchMessage(1)
  3417. def testFetchHeaders(self, uid=0):
  3418. self.function = self.client.fetchHeaders
  3419. self.messages = '9,6,2'
  3420. self.msgObjs = [
  3421. FakeyMessage({'H1': 'V1', 'H2': 'V2'}, (), '', '', 99, None),
  3422. ]
  3423. self.expected = {
  3424. 0: {'RFC822.HEADER': imap4._formatHeaders({'H1': 'V1', 'H2': 'V2'})},
  3425. }
  3426. return self._fetchWork(uid)
  3427. def testFetchHeadersUID(self):
  3428. return self.testFetchHeaders(1)
  3429. def testFetchBody(self, uid=0):
  3430. self.function = self.client.fetchBody
  3431. self.messages = '1,2,3,4,5,6,7'
  3432. self.msgObjs = [
  3433. FakeyMessage({'Header': 'Value'}, (), '', 'Body goes here\r\n', 171, None),
  3434. ]
  3435. self.expected = {
  3436. 0: {'RFC822.TEXT': 'Body goes here\r\n'},
  3437. }
  3438. return self._fetchWork(uid)
  3439. def testFetchBodyUID(self):
  3440. return self.testFetchBody(1)
  3441. def testFetchBodyParts(self):
  3442. """
  3443. Test the server's handling of requests for specific body sections.
  3444. """
  3445. self.function = self.client.fetchSpecific
  3446. self.messages = '1'
  3447. outerBody = ''
  3448. innerBody1 = 'Contained body message text. Squarge.'
  3449. innerBody2 = 'Secondary <i>message</i> text of squarge body.'
  3450. headers = OrderedDict()
  3451. headers['from'] = 'sender@host'
  3452. headers['to'] = 'recipient@domain'
  3453. headers['subject'] = 'booga booga boo'
  3454. headers['content-type'] = 'multipart/alternative; boundary="xyz"'
  3455. innerHeaders = OrderedDict()
  3456. innerHeaders['subject'] = 'this is subject text'
  3457. innerHeaders['content-type'] = 'text/plain'
  3458. innerHeaders2 = OrderedDict()
  3459. innerHeaders2['subject'] = '<b>this is subject</b>'
  3460. innerHeaders2['content-type'] = 'text/html'
  3461. self.msgObjs = [FakeyMessage(
  3462. headers, (), None, outerBody, 123,
  3463. [FakeyMessage(innerHeaders, (), None, innerBody1, None, None),
  3464. FakeyMessage(innerHeaders2, (), None, innerBody2, None, None)])]
  3465. self.expected = {
  3466. 0: [['BODY', ['1'], 'Contained body message text. Squarge.']]}
  3467. def result(R):
  3468. self.result = R
  3469. self.connected.addCallback(
  3470. lambda _: self.function(self.messages, headerNumber=1))
  3471. self.connected.addCallback(result)
  3472. self.connected.addCallback(self._cbStopClient)
  3473. self.connected.addErrback(self._ebGeneral)
  3474. d = loopback.loopbackTCP(self.server, self.client, noisy=False)
  3475. d.addCallback(lambda ign: self.assertEqual(self.result, self.expected))
  3476. return d
  3477. def test_fetchBodyPartOfNonMultipart(self):
  3478. """
  3479. Single-part messages have an implicit first part which clients
  3480. should be able to retrieve explicitly. Test that a client
  3481. requesting part 1 of a text/plain message receives the body of the
  3482. text/plain part.
  3483. """
  3484. self.function = self.client.fetchSpecific
  3485. self.messages = '1'
  3486. parts = [1]
  3487. outerBody = 'DA body'
  3488. headers = OrderedDict()
  3489. headers['from'] = 'sender@host'
  3490. headers['to'] = 'recipient@domain'
  3491. headers['subject'] = 'booga booga boo'
  3492. headers['content-type'] = 'text/plain'
  3493. self.msgObjs = [FakeyMessage(
  3494. headers, (), None, outerBody, 123, None)]
  3495. self.expected = {0: [['BODY', ['1'], 'DA body']]}
  3496. def result(R):
  3497. self.result = R
  3498. self.connected.addCallback(
  3499. lambda _: self.function(self.messages, headerNumber=parts))
  3500. self.connected.addCallback(result)
  3501. self.connected.addCallback(self._cbStopClient)
  3502. self.connected.addErrback(self._ebGeneral)
  3503. d = loopback.loopbackTCP(self.server, self.client, noisy=False)
  3504. d.addCallback(lambda ign: self.assertEqual(self.result, self.expected))
  3505. return d
  3506. def testFetchSize(self, uid=0):
  3507. self.function = self.client.fetchSize
  3508. self.messages = '1:100,2:*'
  3509. self.msgObjs = [
  3510. FakeyMessage({}, (), '', 'x' * 20, 123, None),
  3511. ]
  3512. self.expected = {
  3513. 0: {'RFC822.SIZE': '20'},
  3514. }
  3515. return self._fetchWork(uid)
  3516. def testFetchSizeUID(self):
  3517. return self.testFetchSize(1)
  3518. def testFetchFull(self, uid=0):
  3519. self.function = self.client.fetchFull
  3520. self.messages = '1,3'
  3521. self.msgObjs = [
  3522. FakeyMessage({}, ('\\XYZ', '\\YZX', 'Abc'),
  3523. 'Sun, 25 Jul 2010 06:20:30 -0400 (EDT)',
  3524. 'xyz' * 2, 654, None),
  3525. FakeyMessage({}, ('\\One', '\\Two', 'Three'),
  3526. 'Mon, 14 Apr 2003 19:43:44 -0400',
  3527. 'abc' * 4, 555, None),
  3528. ]
  3529. self.expected = {
  3530. 0: {'FLAGS': ['\\XYZ', '\\YZX', 'Abc'],
  3531. 'INTERNALDATE': '25-Jul-2010 06:20:30 -0400',
  3532. 'RFC822.SIZE': '6',
  3533. 'ENVELOPE': [None, None, [[None, None, None]], [[None, None, None]], None, None, None, None, None, None],
  3534. 'BODY': [None, None, None, None, None, None, '6']},
  3535. 1: {'FLAGS': ['\\One', '\\Two', 'Three'],
  3536. 'INTERNALDATE': '14-Apr-2003 19:43:44 -0400',
  3537. 'RFC822.SIZE': '12',
  3538. 'ENVELOPE': [None, None, [[None, None, None]], [[None, None, None]], None, None, None, None, None, None],
  3539. 'BODY': [None, None, None, None, None, None, '12']},
  3540. }
  3541. return self._fetchWork(uid)
  3542. def testFetchFullUID(self):
  3543. return self.testFetchFull(1)
  3544. def testFetchAll(self, uid=0):
  3545. self.function = self.client.fetchAll
  3546. self.messages = '1,2:3'
  3547. self.msgObjs = [
  3548. FakeyMessage({}, (), 'Mon, 14 Apr 2003 19:43:44 +0400',
  3549. 'Lalala', 10101, None),
  3550. FakeyMessage({}, (), 'Tue, 15 Apr 2003 19:43:44 +0200',
  3551. 'Alalal', 20202, None),
  3552. ]
  3553. self.expected = {
  3554. 0: {'ENVELOPE': [None, None, [[None, None, None]], [[None, None, None]], None, None, None, None, None, None],
  3555. 'RFC822.SIZE': '6',
  3556. 'INTERNALDATE': '14-Apr-2003 19:43:44 +0400',
  3557. 'FLAGS': []},
  3558. 1: {'ENVELOPE': [None, None, [[None, None, None]], [[None, None, None]], None, None, None, None, None, None],
  3559. 'RFC822.SIZE': '6',
  3560. 'INTERNALDATE': '15-Apr-2003 19:43:44 +0200',
  3561. 'FLAGS': []},
  3562. }
  3563. return self._fetchWork(uid)
  3564. def testFetchAllUID(self):
  3565. return self.testFetchAll(1)
  3566. def testFetchFast(self, uid=0):
  3567. self.function = self.client.fetchFast
  3568. self.messages = '1'
  3569. self.msgObjs = [
  3570. FakeyMessage({}, ('\\X',), '19 Mar 2003 19:22:21 -0500', '', 9, None),
  3571. ]
  3572. self.expected = {
  3573. 0: {'FLAGS': ['\\X'],
  3574. 'INTERNALDATE': '19-Mar-2003 19:22:21 -0500',
  3575. 'RFC822.SIZE': '0'},
  3576. }
  3577. return self._fetchWork(uid)
  3578. def testFetchFastUID(self):
  3579. return self.testFetchFast(1)
  3580. class DefaultSearchTests(IMAP4HelperMixin, unittest.TestCase):
  3581. """
  3582. Test the behavior of the server's SEARCH implementation, particularly in
  3583. the face of unhandled search terms.
  3584. """
  3585. def setUp(self):
  3586. self.server = imap4.IMAP4Server()
  3587. self.server.state = 'select'
  3588. self.server.mbox = self
  3589. self.connected = defer.Deferred()
  3590. self.client = SimpleClient(self.connected)
  3591. self.msgObjs = [
  3592. FakeyMessage({}, (), b'', b'', 999, None),
  3593. FakeyMessage({}, (), b'', b'', 10101, None),
  3594. FakeyMessage({}, (), b'', b'', 12345, None),
  3595. FakeyMessage({}, (), b'', b'', 20001, None),
  3596. FakeyMessage({}, (), b'', b'', 20002, None),
  3597. ]
  3598. def fetch(self, messages, uid):
  3599. """
  3600. Pretend to be a mailbox and let C{self.server} lookup messages on me.
  3601. """
  3602. return list(zip(range(1, len(self.msgObjs) + 1), self.msgObjs))
  3603. def _messageSetSearchTest(self, queryTerms, expectedMessages):
  3604. """
  3605. Issue a search with given query and verify that the returned messages
  3606. match the given expected messages.
  3607. @param queryTerms: A string giving the search query.
  3608. @param expectedMessages: A list of the message sequence numbers
  3609. expected as the result of the search.
  3610. @return: A L{Deferred} which fires when the test is complete.
  3611. """
  3612. def search():
  3613. return self.client.search(queryTerms)
  3614. d = self.connected.addCallback(strip(search))
  3615. def searched(results):
  3616. self.assertEqual(results, expectedMessages)
  3617. d.addCallback(searched)
  3618. d.addCallback(self._cbStopClient)
  3619. d.addErrback(self._ebGeneral)
  3620. self.loopback()
  3621. return d
  3622. def test_searchMessageSet(self):
  3623. """
  3624. Test that a search which starts with a message set properly limits
  3625. the search results to messages in that set.
  3626. """
  3627. return self._messageSetSearchTest(b'1', [1])
  3628. def test_searchMessageSetWithStar(self):
  3629. """
  3630. If the search filter ends with a star, all the message from the
  3631. starting point are returned.
  3632. """
  3633. return self._messageSetSearchTest(b'2:*', [2, 3, 4, 5])
  3634. def test_searchMessageSetWithStarFirst(self):
  3635. """
  3636. If the search filter starts with a star, the result should be identical
  3637. with if the filter would end with a star.
  3638. """
  3639. return self._messageSetSearchTest(b'*:2', [2, 3, 4, 5])
  3640. def test_searchMessageSetUIDWithStar(self):
  3641. """
  3642. If the search filter ends with a star, all the message from the
  3643. starting point are returned (also for the SEARCH UID case).
  3644. """
  3645. return self._messageSetSearchTest(b'UID 10000:*', [2, 3, 4, 5])
  3646. def test_searchMessageSetUIDWithStarFirst(self):
  3647. """
  3648. If the search filter starts with a star, the result should be identical
  3649. with if the filter would end with a star (also for the SEARCH UID case).
  3650. """
  3651. return self._messageSetSearchTest(b'UID *:10000', [2, 3, 4, 5])
  3652. def test_searchMessageSetUIDWithStarAndHighStart(self):
  3653. """
  3654. A search filter of 1234:* should include the UID of the last message in
  3655. the mailbox, even if its UID is less than 1234.
  3656. """
  3657. # in our fake mbox the highest message UID is 20002
  3658. return self._messageSetSearchTest(b'UID 30000:*', [5])
  3659. def test_searchMessageSetWithList(self):
  3660. """
  3661. If the search filter contains nesting terms, one of which includes a
  3662. message sequence set with a wildcard, IT ALL WORKS GOOD.
  3663. """
  3664. # 6 is bigger than the biggest message sequence number, but that's
  3665. # okay, because N:* includes the biggest message sequence number even
  3666. # if N is bigger than that (read the rfc nub).
  3667. return self._messageSetSearchTest(b'(6:*)', [5])
  3668. def test_searchOr(self):
  3669. """
  3670. If the search filter contains an I{OR} term, all messages
  3671. which match either subexpression are returned.
  3672. """
  3673. return self._messageSetSearchTest(b'OR 1 2', [1, 2])
  3674. def test_searchOrMessageSet(self):
  3675. """
  3676. If the search filter contains an I{OR} term with a
  3677. subexpression which includes a message sequence set wildcard,
  3678. all messages in that set are considered for inclusion in the
  3679. results.
  3680. """
  3681. return self._messageSetSearchTest(b'OR 2:* 2:*', [2, 3, 4, 5])
  3682. def test_searchNot(self):
  3683. """
  3684. If the search filter contains a I{NOT} term, all messages
  3685. which do not match the subexpression are returned.
  3686. """
  3687. return self._messageSetSearchTest(b'NOT 3', [1, 2, 4, 5])
  3688. def test_searchNotMessageSet(self):
  3689. """
  3690. If the search filter contains a I{NOT} term with a
  3691. subexpression which includes a message sequence set wildcard,
  3692. no messages in that set are considered for inclusion in the
  3693. result.
  3694. """
  3695. return self._messageSetSearchTest(b'NOT 2:*', [1])
  3696. def test_searchAndMessageSet(self):
  3697. """
  3698. If the search filter contains multiple terms implicitly
  3699. conjoined with a message sequence set wildcard, only the
  3700. intersection of the results of each term are returned.
  3701. """
  3702. return self._messageSetSearchTest(b'2:* 3', [3])
  3703. def test_searchInvalidCriteria(self):
  3704. """
  3705. If the search criteria is not a valid key, a NO result is returned to
  3706. the client (resulting in an error callback), and an IllegalQueryError is
  3707. logged on the server side.
  3708. """
  3709. queryTerms = b'FOO'
  3710. def search():
  3711. return self.client.search(queryTerms)
  3712. d = self.connected.addCallback(strip(search))
  3713. d = self.assertFailure(d, imap4.IMAP4Exception)
  3714. def errorReceived(results):
  3715. """
  3716. Verify that the server logs an IllegalQueryError and the
  3717. client raises an IMAP4Exception with 'Search failed:...'
  3718. """
  3719. self.client.transport.loseConnection()
  3720. self.server.transport.loseConnection()
  3721. # Check what the server logs
  3722. errors = self.flushLoggedErrors(imap4.IllegalQueryError)
  3723. self.assertEqual(len(errors), 1)
  3724. # Verify exception given to client has the correct message
  3725. self.assertEqual(
  3726. b"SEARCH failed: Invalid search command FOO", networkString(str(results)))
  3727. d.addCallback(errorReceived)
  3728. d.addErrback(self._ebGeneral)
  3729. self.loopback()
  3730. return d
  3731. @implementer(imap4.ISearchableMailbox)
  3732. class FetchSearchStoreTests(unittest.TestCase, IMAP4HelperMixin):
  3733. def setUp(self):
  3734. self.expected = self.result = None
  3735. self.server_received_query = None
  3736. self.server_received_uid = None
  3737. self.server_received_parts = None
  3738. self.server_received_messages = None
  3739. self.server = imap4.IMAP4Server()
  3740. self.server.state = 'select'
  3741. self.server.mbox = self
  3742. self.connected = defer.Deferred()
  3743. self.client = SimpleClient(self.connected)
  3744. def search(self, query, uid):
  3745. # Look for a specific bad query, so we can verify we handle it properly
  3746. if query == [b'FOO']:
  3747. raise imap4.IllegalQueryError("FOO is not a valid search criteria")
  3748. self.server_received_query = query
  3749. self.server_received_uid = uid
  3750. return self.expected
  3751. def addListener(self, *a, **kw):
  3752. pass
  3753. removeListener = addListener
  3754. def _searchWork(self, uid):
  3755. def search():
  3756. return self.client.search(self.query, uid=uid)
  3757. def result(R):
  3758. self.result = R
  3759. self.connected.addCallback(strip(search)
  3760. ).addCallback(result
  3761. ).addCallback(self._cbStopClient
  3762. ).addErrback(self._ebGeneral)
  3763. def check(ignored):
  3764. # Ensure no short-circuiting weirdness is going on
  3765. self.assertFalse(self.result is self.expected)
  3766. self.assertEqual(self.result, self.expected)
  3767. self.assertEqual(self.uid, self.server_received_uid)
  3768. self.assertEqual(
  3769. imap4.parseNestedParens(self.query),
  3770. self.server_received_query
  3771. )
  3772. d = loopback.loopbackTCP(self.server, self.client, noisy=False)
  3773. d.addCallback(check)
  3774. return d
  3775. def testSearch(self):
  3776. self.query = imap4.Or(
  3777. imap4.Query(header=('subject', 'substring')),
  3778. imap4.Query(larger=1024, smaller=4096),
  3779. )
  3780. self.expected = [1, 4, 5, 7]
  3781. self.uid = 0
  3782. return self._searchWork(0)
  3783. def testUIDSearch(self):
  3784. self.query = imap4.Or(
  3785. imap4.Query(header=('subject', 'substring')),
  3786. imap4.Query(larger=1024, smaller=4096),
  3787. )
  3788. self.uid = 1
  3789. self.expected = [1, 2, 3]
  3790. return self._searchWork(1)
  3791. def getUID(self, msg):
  3792. try:
  3793. return self.expected[msg]['UID']
  3794. except (TypeError, IndexError):
  3795. return self.expected[msg-1]
  3796. except KeyError:
  3797. return 42
  3798. def fetch(self, messages, uid):
  3799. self.server_received_uid = uid
  3800. self.server_received_messages = str(messages)
  3801. return self.expected
  3802. def _fetchWork(self, fetch):
  3803. def result(R):
  3804. self.result = R
  3805. self.connected.addCallback(strip(fetch)
  3806. ).addCallback(result
  3807. ).addCallback(self._cbStopClient
  3808. ).addErrback(self._ebGeneral)
  3809. def check(ignored):
  3810. # Ensure no short-circuiting weirdness is going on
  3811. self.assertFalse(self.result is self.expected)
  3812. self.parts and self.parts.sort()
  3813. self.server_received_parts and self.server_received_parts.sort()
  3814. if self.uid:
  3815. for (k, v) in self.expected.items():
  3816. v['UID'] = str(k)
  3817. self.assertEqual(self.result, self.expected)
  3818. self.assertEqual(self.uid, self.server_received_uid)
  3819. self.assertEqual(self.parts, self.server_received_parts)
  3820. self.assertEqual(imap4.parseIdList(self.messages),
  3821. imap4.parseIdList(self.server_received_messages))
  3822. d = loopback.loopbackTCP(self.server, self.client, noisy=False)
  3823. d.addCallback(check)
  3824. return d
  3825. def test_invalidTerm(self):
  3826. """
  3827. If, as part of a search, an ISearchableMailbox raises an
  3828. IllegalQueryError (e.g. due to invalid search criteria), client sees a
  3829. failure response, and an IllegalQueryError is logged on the server.
  3830. """
  3831. query = b'FOO'
  3832. def search():
  3833. return self.client.search(query)
  3834. d = self.connected.addCallback(strip(search))
  3835. d = self.assertFailure(d, imap4.IMAP4Exception)
  3836. def errorReceived(results):
  3837. """
  3838. Verify that the server logs an IllegalQueryError and the
  3839. client raises an IMAP4Exception with 'Search failed:...'
  3840. """
  3841. self.client.transport.loseConnection()
  3842. self.server.transport.loseConnection()
  3843. # Check what the server logs
  3844. errors = self.flushLoggedErrors(imap4.IllegalQueryError)
  3845. self.assertEqual(len(errors), 1)
  3846. # Verify exception given to client has the correct message
  3847. self.assertEqual(
  3848. "SEARCH failed: FOO is not a valid search criteria",
  3849. str(results))
  3850. d.addCallback(errorReceived)
  3851. d.addErrback(self._ebGeneral)
  3852. self.loopback()
  3853. return d
  3854. class FakeMailbox:
  3855. def __init__(self):
  3856. self.args = []
  3857. def addMessage(self, body, flags, date):
  3858. self.args.append((body, flags, date))
  3859. return defer.succeed(None)
  3860. @implementer(imap4.IMessageFile)
  3861. class FeaturefulMessage:
  3862. def getFlags(self):
  3863. return 'flags'
  3864. def getInternalDate(self):
  3865. return 'internaldate'
  3866. def open(self):
  3867. return BytesIO(b"open")
  3868. @implementer(imap4.IMessageCopier)
  3869. class MessageCopierMailbox:
  3870. def __init__(self):
  3871. self.msgs = []
  3872. def copy(self, msg):
  3873. self.msgs.append(msg)
  3874. return len(self.msgs)
  3875. class CopyWorkerTests(unittest.TestCase):
  3876. def testFeaturefulMessage(self):
  3877. s = imap4.IMAP4Server()
  3878. # Yes. I am grabbing this uber-non-public method to test it.
  3879. # It is complex. It needs to be tested directly!
  3880. # Perhaps it should be refactored, simplified, or split up into
  3881. # not-so-private components, but that is a task for another day.
  3882. # Ha ha! Addendum! Soon it will be split up, and this test will
  3883. # be re-written to just use the default adapter for IMailbox to
  3884. # IMessageCopier and call .copy on that adapter.
  3885. f = s._IMAP4Server__cbCopy
  3886. m = FakeMailbox()
  3887. d = f([(i, FeaturefulMessage()) for i in range(1, 11)], 'tag', m)
  3888. def cbCopy(results):
  3889. for a in m.args:
  3890. self.assertEqual(a[0].read(), b"open")
  3891. self.assertEqual(a[1], "flags")
  3892. self.assertEqual(a[2], "internaldate")
  3893. for (status, result) in results:
  3894. self.assertTrue(status)
  3895. self.assertEqual(result, None)
  3896. return d.addCallback(cbCopy)
  3897. def testUnfeaturefulMessage(self):
  3898. s = imap4.IMAP4Server()
  3899. # See above comment
  3900. f = s._IMAP4Server__cbCopy
  3901. m = FakeMailbox()
  3902. msgs = [FakeyMessage({b'Header-Counter': intToBytes(i)}, (), b'Date', b'Body ' + intToBytes(i), i + 10, None) for i in range(1, 11)]
  3903. d = f([im for im in zip(range(1, 11), msgs)], 'tag', m)
  3904. def cbCopy(results):
  3905. seen = []
  3906. for a in m.args:
  3907. seen.append(a[0].read())
  3908. self.assertEqual(a[1], ())
  3909. self.assertEqual(a[2], b"Date")
  3910. seen.sort()
  3911. exp = [b"Header-Counter: " + intToBytes(i) + b"\r\n\r\nBody " +intToBytes(i) for i in range(1, 11)]
  3912. exp.sort()
  3913. self.assertEqual(seen, exp)
  3914. for (status, result) in results:
  3915. self.assertTrue(status)
  3916. self.assertEqual(result, None)
  3917. return d.addCallback(cbCopy)
  3918. def testMessageCopier(self):
  3919. s = imap4.IMAP4Server()
  3920. # See above comment
  3921. f = s._IMAP4Server__cbCopy
  3922. m = MessageCopierMailbox()
  3923. msgs = [object() for i in range(1, 11)]
  3924. d = f([im for im in zip(range(1, 11), msgs)], b'tag', m)
  3925. def cbCopy(results):
  3926. self.assertEqual(results, zip([1] * 10, range(1, 11)))
  3927. for (orig, new) in zip(msgs, m.msgs):
  3928. self.assertIdentical(orig, new)
  3929. return d.addCallback(cbCopy)
  3930. class TLSTests(IMAP4HelperMixin, unittest.TestCase):
  3931. serverCTX = ServerTLSContext and ServerTLSContext()
  3932. clientCTX = ClientTLSContext and ClientTLSContext()
  3933. def loopback(self):
  3934. return loopback.loopbackTCP(self.server, self.client, noisy=False)
  3935. def testAPileOfThings(self):
  3936. SimpleServer.theAccount.addMailbox(b'inbox')
  3937. called = []
  3938. def login():
  3939. called.append(None)
  3940. return self.client.login(b'testuser', b'password-test')
  3941. def list():
  3942. called.append(None)
  3943. return self.client.list(b'inbox', b'%')
  3944. def status():
  3945. called.append(None)
  3946. return self.client.status(b'inbox', b'UIDNEXT')
  3947. def examine():
  3948. called.append(None)
  3949. return self.client.examine(b'inbox')
  3950. def logout():
  3951. called.append(None)
  3952. return self.client.logout()
  3953. self.client.requireTransportSecurity = True
  3954. methods = [login, list, status, examine, logout]
  3955. map(self.connected.addCallback, map(strip, methods))
  3956. self.connected.addCallbacks(self._cbStopClient, self._ebGeneral)
  3957. def check(ignored):
  3958. self.assertEqual(self.server.startedTLS, True)
  3959. self.assertEqual(self.client.startedTLS, True)
  3960. self.assertEqual(len(called), len(methods))
  3961. d = self.loopback()
  3962. d.addCallback(check)
  3963. return d
  3964. def testLoginLogin(self):
  3965. self.server.checker.addUser(b'testuser', b'password-test')
  3966. success = []
  3967. self.client.registerAuthenticator(imap4.LOGINAuthenticator(b'testuser'))
  3968. self.connected.addCallback(
  3969. lambda _: self.client.authenticate(b'password-test')
  3970. ).addCallback(
  3971. lambda _: self.client.logout()
  3972. ).addCallback(success.append
  3973. ).addCallback(self._cbStopClient
  3974. ).addErrback(self._ebGeneral)
  3975. d = self.loopback()
  3976. d.addCallback(lambda x : self.assertEqual(len(success), 1))
  3977. return d
  3978. def test_startTLS(self):
  3979. """
  3980. L{IMAP4Client.startTLS} triggers TLS negotiation and returns a
  3981. L{Deferred} which fires after the client's transport is using
  3982. encryption.
  3983. """
  3984. success = []
  3985. self.connected.addCallback(lambda _: self.client.startTLS())
  3986. def checkSecure(ignored):
  3987. self.assertTrue(
  3988. interfaces.ISSLTransport.providedBy(self.client.transport))
  3989. self.connected.addCallback(checkSecure)
  3990. self.connected.addCallback(self._cbStopClient)
  3991. self.connected.addCallback(success.append)
  3992. self.connected.addErrback(self._ebGeneral)
  3993. d = self.loopback()
  3994. d.addCallback(lambda x : self.assertTrue(success))
  3995. return defer.gatherResults([d, self.connected])
  3996. def testFailedStartTLS(self):
  3997. failures = []
  3998. def breakServerTLS(ign):
  3999. self.server.canStartTLS = False
  4000. self.connected.addCallback(breakServerTLS)
  4001. self.connected.addCallback(lambda ign: self.client.startTLS())
  4002. self.connected.addErrback(
  4003. lambda err: failures.append(err.trap(imap4.IMAP4Exception)))
  4004. self.connected.addCallback(self._cbStopClient)
  4005. self.connected.addErrback(self._ebGeneral)
  4006. def check(ignored):
  4007. self.assertTrue(failures)
  4008. self.assertIdentical(failures[0], imap4.IMAP4Exception)
  4009. return self.loopback().addCallback(check)
  4010. class SlowMailbox(SimpleMailbox):
  4011. howSlow = 2
  4012. callLater = None
  4013. fetchDeferred = None
  4014. # Not a very nice implementation of fetch(), but it'll
  4015. # do for the purposes of testing.
  4016. def fetch(self, messages, uid):
  4017. d = defer.Deferred()
  4018. self.callLater(self.howSlow, d.callback, ())
  4019. self.fetchDeferred.callback(None)
  4020. return d
  4021. class TimeoutTests(IMAP4HelperMixin, unittest.TestCase):
  4022. def test_serverTimeout(self):
  4023. """
  4024. The *client* has a timeout mechanism which will close connections that
  4025. are inactive for a period.
  4026. """
  4027. c = Clock()
  4028. self.server.timeoutTest = True
  4029. self.client.timeout = 5 #seconds
  4030. self.client.callLater = c.callLater
  4031. self.selectedArgs = None
  4032. def login():
  4033. d = self.client.login(b'testuser', b'password-test')
  4034. c.advance(5)
  4035. d.addErrback(timedOut)
  4036. return d
  4037. def timedOut(failure):
  4038. self._cbStopClient(None)
  4039. failure.trap(error.TimeoutError)
  4040. d = self.connected.addCallback(strip(login))
  4041. d.addErrback(self._ebGeneral)
  4042. return defer.gatherResults([d, self.loopback()])
  4043. def test_longFetchDoesntTimeout(self):
  4044. """
  4045. The connection timeout does not take effect during fetches.
  4046. """
  4047. c = Clock()
  4048. SlowMailbox.callLater = c.callLater
  4049. SlowMailbox.fetchDeferred = defer.Deferred()
  4050. self.server.callLater = c.callLater
  4051. SimpleServer.theAccount.mailboxFactory = SlowMailbox
  4052. SimpleServer.theAccount.addMailbox('mailbox-test')
  4053. self.server.setTimeout(1)
  4054. def login():
  4055. return self.client.login(b'testuser', b'password-test')
  4056. def select():
  4057. self.server.setTimeout(1)
  4058. return self.client.select('mailbox-test')
  4059. def fetch():
  4060. return self.client.fetchUID(b'1:*')
  4061. def stillConnected():
  4062. self.assertNotEqual(self.server.state, 'timeout')
  4063. def cbAdvance(ignored):
  4064. for i in range(4):
  4065. c.advance(.5)
  4066. SlowMailbox.fetchDeferred.addCallback(cbAdvance)
  4067. d1 = self.connected.addCallback(strip(login))
  4068. d1.addCallback(strip(select))
  4069. d1.addCallback(strip(fetch))
  4070. d1.addCallback(strip(stillConnected))
  4071. d1.addCallback(self._cbStopClient)
  4072. d1.addErrback(self._ebGeneral)
  4073. d = defer.gatherResults([d1, self.loopback()])
  4074. return d
  4075. def test_idleClientDoesDisconnect(self):
  4076. """
  4077. The *server* has a timeout mechanism which will close connections that
  4078. are inactive for a period.
  4079. """
  4080. c = Clock()
  4081. # Hook up our server protocol
  4082. transport = StringTransportWithDisconnection()
  4083. transport.protocol = self.server
  4084. self.server.callLater = c.callLater
  4085. self.server.makeConnection(transport)
  4086. # Make sure we can notice when the connection goes away
  4087. lost = []
  4088. connLost = self.server.connectionLost
  4089. self.server.connectionLost = lambda reason: (lost.append(None), connLost(reason))[1]
  4090. # 2/3rds of the idle timeout elapses...
  4091. c.pump([0.0] + [self.server.timeOut / 3.0] * 2)
  4092. self.assertFalse(lost, lost)
  4093. # Now some more
  4094. c.pump([0.0, self.server.timeOut / 2.0])
  4095. self.assertTrue(lost)
  4096. class DisconnectionTests(unittest.TestCase):
  4097. def testClientDisconnectFailsDeferreds(self):
  4098. c = imap4.IMAP4Client()
  4099. t = StringTransportWithDisconnection()
  4100. c.makeConnection(t)
  4101. d = self.assertFailure(c.login(b'testuser', 'example.com'), error.ConnectionDone)
  4102. c.connectionLost(error.ConnectionDone("Connection closed"))
  4103. return d
  4104. class SynchronousMailbox(object):
  4105. """
  4106. Trivial, in-memory mailbox implementation which can produce a message
  4107. synchronously.
  4108. """
  4109. def __init__(self, messages):
  4110. self.messages = messages
  4111. def fetch(self, msgset, uid):
  4112. assert not uid, "Cannot handle uid requests."
  4113. for msg in msgset:
  4114. yield msg, self.messages[msg - 1]
  4115. class StringTransportConsumer(StringTransport):
  4116. producer = None
  4117. streaming = None
  4118. def registerProducer(self, producer, streaming):
  4119. self.producer = producer
  4120. self.streaming = streaming
  4121. class PipeliningTests(unittest.TestCase):
  4122. """
  4123. Tests for various aspects of the IMAP4 server's pipelining support.
  4124. """
  4125. messages = [
  4126. FakeyMessage({}, [], b'', b'0', None, None),
  4127. FakeyMessage({}, [], b'', b'1', None, None),
  4128. FakeyMessage({}, [], b'', b'2', None, None),
  4129. ]
  4130. def setUp(self):
  4131. self.iterators = []
  4132. self.transport = StringTransportConsumer()
  4133. self.server = imap4.IMAP4Server(None, None, self.iterateInReactor)
  4134. self.server.makeConnection(self.transport)
  4135. def iterateInReactor(self, iterator):
  4136. d = defer.Deferred()
  4137. self.iterators.append((iterator, d))
  4138. return d
  4139. def tearDown(self):
  4140. self.server.connectionLost(failure.Failure(error.ConnectionDone()))
  4141. def test_synchronousFetch(self):
  4142. """
  4143. Test that pipelined FETCH commands which can be responded to
  4144. synchronously are responded to correctly.
  4145. """
  4146. mailbox = SynchronousMailbox(self.messages)
  4147. # Skip over authentication and folder selection
  4148. self.server.state = 'select'
  4149. self.server.mbox = mailbox
  4150. # Get rid of any greeting junk
  4151. self.transport.clear()
  4152. # Here's some pipelined stuff
  4153. self.server.dataReceived(
  4154. b'01 FETCH 1 BODY[]\r\n'
  4155. b'02 FETCH 2 BODY[]\r\n'
  4156. b'03 FETCH 3 BODY[]\r\n')
  4157. # Flush anything the server has scheduled to run
  4158. while self.iterators:
  4159. for e in self.iterators[0][0]:
  4160. break
  4161. else:
  4162. self.iterators.pop(0)[1].callback(None)
  4163. # The bodies are empty because we aren't simulating a transport
  4164. # exactly correctly (we have StringTransportConsumer but we never
  4165. # call resumeProducing on its producer). It doesn't matter: just
  4166. # make sure the surrounding structure is okay, and that no
  4167. # exceptions occurred.
  4168. self.assertEqual(
  4169. self.transport.value(),
  4170. b'* 1 FETCH (BODY[] )\r\n'
  4171. b'01 OK FETCH completed\r\n'
  4172. b'* 2 FETCH (BODY[] )\r\n'
  4173. b'02 OK FETCH completed\r\n'
  4174. b'* 3 FETCH (BODY[] )\r\n'
  4175. b'03 OK FETCH completed\r\n')
  4176. if ClientTLSContext is None:
  4177. for case in (TLSTests,):
  4178. case.skip = "OpenSSL not present"
  4179. elif interfaces.IReactorSSL(reactor, None) is None:
  4180. for case in (TLSTests,):
  4181. case.skip = "Reactor doesn't support SSL"
  4182. class IMAP4ServerFetchTests(unittest.TestCase):
  4183. """
  4184. This test case is for the FETCH tests that require
  4185. a C{StringTransport}.
  4186. """
  4187. def setUp(self):
  4188. self.transport = StringTransport()
  4189. self.server = imap4.IMAP4Server()
  4190. self.server.state = 'select'
  4191. self.server.makeConnection(self.transport)
  4192. def test_fetchWithPartialValidArgument(self):
  4193. """
  4194. If by any chance, extra bytes got appended at the end of a valid
  4195. FETCH arguments, the client should get a BAD - arguments invalid
  4196. response.
  4197. See U{RFC 3501<http://tools.ietf.org/html/rfc3501#section-6.4.5>},
  4198. section 6.4.5,
  4199. """
  4200. # We need to clear out the welcome message.
  4201. self.transport.clear()
  4202. # Let's send out the faulty command.
  4203. self.server.dataReceived(b"0001 FETCH 1 FULLL\r\n")
  4204. expected = b"0001 BAD Illegal syntax: Invalid Argument\r\n"
  4205. self.assertEqual(self.transport.value(), expected)
  4206. self.transport.clear()
  4207. self.server.connectionLost(error.ConnectionDone("Connection closed"))