python-messaging-0.5.12/0000775000175000017500000000000012041141314014632 5ustar achiangachiangpython-messaging-0.5.12/README0000664000175000017500000000226011707534031015524 0ustar achiangachiangWhat is python-messaging? ========================= A SMS/MMS encoder/decoder written 100% in Python. History ======= Originally written by Paul Hardwick with some bits from Dave Berkeley's pysms. It was imported in the Wader and MobileManager projects and quickly became clear that a joint effort would be more beneficial for everyone. In 2010, the superb python-mms package by Francois Aucamp was merged into python-messaging to provide a complete SMS/MMS encoder/decoder. In 2011, as part of a license clarification, Francis Aucamp was contacted and asked if it were possible to relicence his MMS portions as GPLv2, he responded: "feel free to re-license the parts of the python-mms code you are using in your python-messaging project to GPL v2; you have my full consent." SMS Features ============ * Supports 7bit, 8bit and UCS2 encodings * Multipart encoding/decoding * Status report encoding/decoding * Relative validity * Alphanumeric address decoding * Supports python 2.5 up to 3.2 * Tests MMS Features ============ * SMIL support * Main formats supported: jpg, gif, arm, 3gp, midi, etc. * Supports MMS 1.0-1.4 decoding/encoding * Supports python 2.5 up to 3.2 * Tests python-messaging-0.5.12/messaging/0000775000175000017500000000000011707534031016621 5ustar achiangachiangpython-messaging-0.5.12/messaging/test/0000775000175000017500000000000011707534031017600 5ustar achiangachiangpython-messaging-0.5.12/messaging/test/mms-data/0000775000175000017500000000000011707534031021303 5ustar achiangachiangpython-messaging-0.5.12/messaging/test/mms-data/projekt_exempel.mms0000664000175000017500000000470211707534031025221 0ustar achiangachiang4-fc60@12345/TYPE=PLMNHejapplication/smil" mms.txt"mms.txtJonatan är en GNU,cSonyhEr.gif"SonyhEr.gifGIF89aePIUIUIUIU%U%UnIUU%UےUn%UUInU%In%U%nUnI%%InIU%%IInI%U%ےnInn%n%۷n%UIInIn%II%n%nII%%U%%%%IIIInn%%UnnIIU%%I%Inn%II%nInInI%U%n%nnIn%In%IUI%%IU%In%In۪nU%۪I۪%nU%UIn۪U%UnUU%UInU۪%U%UIUIUIUIUnI%%nU%nU%%nU۪II%nUInUnUnI%IUnUInUnUnnII%%UUUUnnU۪۷Un۷UےnIUnU۪nII%ےUnInUUIn%I۷%nU%%nU%nUnI%,eP H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ4*PS%MgJ)M8:qJi$JՏ8?Y>BMVd=BT]SDkTlBe"ŷ(YŪnA E/Q UiT%3PQB\ hQ"P壙eA)4MhQ\>揺XjTdիLXBH$MEʝju<@+D`UBEUMeZuFaKRTQAEc*)I EjZR@" <*H4WPpe Ҹ|8Q4e -bT\S XVHu='*LB^HKC$5 r %RJQD!r_Q< hQbVX5mRy5BK+zXhIͥD4@FrɭJӠ BH(w _dN9iȥRPcR$ (Bv+^ziOiH,QPqR2"Ū*J]fS:Ud2 RH&4ʪ IjiLkȻJ&K,!kQrDE2 ܜ{ɂ֣k0ȎJLlWjΚ+ *Šk3<ňh,B)MT==uG$(8_4)ʞmɶ* J1O,S9Soy`4[EIO#6TDΈGQȐ?2|+E.M;4ˉ|-$gTMyr҂J&S]0WcK ,BK8vj}U@I2ӏ4.' X]d Dr6 J UZg@P x^ 9Bx%.׵aNwF8(d.|3=|~UP` HvCu0!~AqXxDEAш2c*z` H2hL6pHǃ;'}application/smilmms.smil" python-messaging-0.5.12/messaging/test/mms-data/gallery2test.mms0000664000175000017500000004512411707534031024450 0ustar achiangachiang1118775337+16505550000/TYPE=PLMNJgJgjapplication/smil"application/smil" gnu-head *text/plaincharsetiso-8859-1"Jgj*[image/jpegnamegnu-head.jpg"JFIFJPEG:gnu-head.jpg 276x261C   %# , #&')*)-0-(0%()( ?!1"AQa#2q3BR$Cbr%4Scs?8pe-_d)oݵ:ě6W d|X7;=οT1Xy[pEueQH#uy:gn2אHA_qqqr35tqзڰ~TfV R֩<˿dB?>~n-`̊ +-`YXd$ή88MԘǛӇJ(A'O!eֲAјYT2 ::2?.G.e4Pz[4@5 eЪ[_Zvc7-څ.֞BXM$ ^TL2Rkz0c,$+y#jһHήPu.Y$zكf-^G)$A$ [1v5= B$,T韻_ o~BP^;NR~5D%Nk@R/nʀ 4k]O~S!:!č /==rsXonCr9`T7|: i+4-"㼁BGqqteKV %a*H,vGܤu=?j$,cCyW:GG^{Y |t ~ -8,C` h7Yuཌྷ;( k827 t6gT:\=y,5ombO;*eeTC3/]u*]ZC>7bY${A$pTAW ׇތ?C߱(flثiW;E}Ƞl\ڣ4zWŎڏu-@2|x(6Q>@oD8㕎)(Hp{1jӑP e`d`J6H^KVZ^G)f"#8# <*ޠd2KNޝBosF`$hTB5tGo gY՘B$o5%*~#VfWyz~$ɗVZ$Lqw;n|r,8vKvl؜/bedUR@!C^k:cAZWEF, uBf_ͯ)zsov 2}R8$IBB &8yIt /EXBѯ-i1 @lѾ~rlMז+ZIuWՠ{dF=wЕL%N4Hq+1۝n1%B;|otLgi_ .3=S6CkQ9y{{۹;)Ӈ3HE<I>U% Zbj{6J:bA#AG?,yuXM~rIBTH`LM@%] (b VOբ @zkJ'kP=/رGWFNci ֘$%N95^ZU+t\q>kDqtC`ӆ1ҋh6s$@6A;aH.qԩuJumv(^럫wVֻf8*D:wJ?R4, l猝֎jWAdTK|$l\U r7Ԭ>889Zt?L=XJBQR?5jGVF;R[ֹiKk7|oy9+MuF{ AM%zNPs]0X2מ~ҷi*]]ZZ5ދ#,'Ycq[RAqCLI$V2R!aD} mUQ&cUCO&[⢕\#2>ĶR5/Uv$WHbIZIc޴li$c?pe#ʤM0eIn9ɿi'r$ #]+Pݩ k΋,n ~y?.ʊY '@s<苧zU$.~? +!Q㸾TQ x1e -LznF&3ӿnƎ_}Qbeq4cAmg=pI]N;i?!Yc'r)y%tp>[gLJ꘩Xd5;A6ltŘFIXֻ5>89L6HeSF ذOrc)aӝa@Dpο!( L/oװ/e[o0}MTXe" ~`̠Իڟ؀G&889l}|:R?!  AsޚX.03JD RJ3Ȥ*/rf{& \"GyzjHM$UbAR #S).S"uG9 nE?Ttft6}Ϩx*$4IVNĺs5rDuF%ؖ5޾8~o+O ؎*$t~$G3 "r*p;Ӳ's˽M* Vc$ ^;и`TH2 kaTSާK[1dUlÀbFb̬Iga3||'rT  @B9Fiyt&S 6?G @2LG'kk~juj>0Կ(A7o]_'?NJ~]zW[zkO=LS' "שvBH#6.WcƐzjqgN^Jle7J4R `W"H#`*}"JO$}{SKV.L4I&/IQ UG' 0y$3qfm,W"s^pyRlLreޟumJ(_UfQy\v.6FjRJ"F,^m767鬗Yݣf[c;}h$7ƹ7ѝ/5?Qީ׮ۖp4W~f>y_M%K=kva0oK$2$C..B)TX}Mꦅ{ZlN6INovuT|ц#T{Q TVY6le> o0=>kY9v&II@B>vxNdg0О&SbĖh# @ ;{:хMV,ITF~`#/q'Y20P0_!MxUAfd U󭲃']tGC EZl uY 2l [2rN gt^Ic<-دp=cW+ƽŎZfVaA⚡YHf (H4`|ʫ|;~%莥垙jU>Wgfb44k5L]l=(6|A9]pa];K*oVYuYJBz!WgAߗѮt7&D~)TSB%|RyY6$,E[fQU,:'$Ul 8|Pr8@QT'|Ͻj=PڜOE!BD!=4 hKNޓڞ1VI'J-I,;@"yUIRM|_#gQ:Y n>}Iw1loPelEU"R=u#=;F %5ڰ:Y]l*vNC >A9AGzn)2N2yD`¾*u]kØ@-}m"ePNP zl\yÕ0ԚvkKRH F|*)f ($é/ܡ+ۺJIc_|PwĂR҂Ē\zrNܯ MGTD$.zaOlvn50m2&p0D0' _Zz:9Hjԡf|~9|-<\,c4s)kZ 5(cJ5‚( 50 QBʎkZּk~5u#BrxN;ejV=奕bl(@WPʢq=[rzՓ>gJY%TPJ{RC{RI=(!WJ b֞d;#-Mr!@ F Tcm*ߕs2]N2\qqvm;$wc,DZDnWb;H4aTih?n-aHRSP$Cqry22#z%dHx kN k 8i7osijxS:G9,VVd,/Rd:'[FgԞb~Fc3it47H@]w㘇oNz*YJhĭ q:=_>dw:gONF{"ħLXTȀ}y:z,b![נrJђj&0ĺB wrޟdٚ\ɲ1;oaw~5Zu vu(d"2ѳr#DAYr/VwdQ`SFp/j$'lH]`/_X?B Tmivf?vy;2PFYˆ"2% K,#&M}aunnTX,ı&Vچ4+0޾ӽ髣2=%Mc8'+Vٌ~CvA>6!: ܬ"bB&ƻdEtH<}<l9sy;f KHݪUt}Ćo.U:ݰ$fXǒW^$ f>W5Xx>gzwVHLAI 55@lGWV莊筩qY!#dlltwH޹PW FK#ظ+fy!6T=ӂil>nGŔg34K/n oR$51{fjCJ'4C!6IF-vR+wnT!["=a'H4El oy!N(ݚAfHmSq=2y*߫8z(Њ#JJ#f&7$H`kؼ_黌h-Av_ᛸ$`ԙ\.#xV'MT)+gU9CN:B`f>vZDf;h/cWW$S]@;Q]MWavdTgDf@TY:j(8QUQsӎCuOꞙa2kV&&:R| C.|;cPѸ!ڙ6B#@aes⫉֝X&-HOqfo<;-btF#21K{A[ib ,C+K* wb8kQWU@U28=Ab: ݼ?NdeKQV8m=cp@Pݿ_;_YnnAiEi} ;jrOę@:>̼?¡<2 Q^hXx|1N 1(U@?vqOAw:"ݼUid%+W'y$`7 ǚw@ucc?S-3[삱?^h@ܿz&4K*^M $XU}o]Q:1'BMd)m]*Wyց Hs$MG) iь)G3)Q! QYUz*K3`n`[j.,dJJ˥ic2 I#:S)SmJ$CWU+457ϝb^̔vԖ}X+DE'Dߍ?j=sWZMR {WLs.*>|9}4X8ƼVX8!$x2` ˇZS鞼UqY+K YBl9,>ηXe+-OhUm׆=xӆPOc]O^MUUŶ*>Yά?M~$jK@ڨe0W GiF7LF^o]q^+K kgB4A8׀Gcj%^;$S!5u4/rD N:zDꌅktd*㬾 v_vt8؎^#dhGb8{}uHb vDyQN_ɵ*V҃5꫼ T@I_|J<|,7eǹ$ݡYBG`D\iQ3ZEN鄇eŎI"fs clbB7qK!*69KձgO\ +3w2 }H@GtFhy讐A,i<~BRF؂3B3w~nܝZ91CO Xr.nbi)V;atcEf׍tMg^V2ԭ]ZU@.׶; 1؁ȿRtt$:kZtey ێ0ll'[tG9z{#Jl>_覤Tn!eDf!>k8\ %{(-腑wNO>o_Ja: X{nIe]Di-. pe?xr89 69`6.rrrUVDH 4y)6s\OD+%ƣ^U[jH=Pht-{iNXk+wiJ89,Q)bI GES8?JKSOeeu4 @:(J+38{eKhojpuܦĝ>rq_X?Q/C^:;v)m 33_7~2c+IHv 22$,WQapc\rPdf_qHDlk[}RUj^("$Hsfv',s"(b@'@lGW+w0uKYjkf 2H #ccc|q>[;=bO?]Ek&l봀$ ~4Fӽz;W(38 o* v q0Wzy ^Jj[j@)h#j;}$Kc:8}=*![0gS3{[$rÊE{ AAb@ W?5I$˒?У~B'qq3tk24Ѹ|e̔CX $ǜl8zzo:{]V"}D/695w:n.IylXm5*ZCHA/Y!4IUckVodD8Ŕ,lJƈ 0#X>6#P̍1(ѬLvbo$>u`C\N|mđWg`"A:$1a5tşv$"],P9:b? ;g IzXFxp6c$yڟ| W A=C7S_a^r촌řɐMUctZ{ O)IĀwP:$R $vkll**Z7V `G":8BK$>xVARMBDrqqrǢs>'g?2,"6oWDpz?nefQ >58+kiᱰ$xKm3j6w},G^I2"ܿD[[-fE&.;'yןOqqs08X~z2waObCheu؞Uje![תWJVuNF)`Bݫ"ZA' ?btՋ3Pz(US;$ܫ4~Gn fzR ejp0ג3ѲvfbEU ՁrUfBd%J>Ѱǚwޑ?D=G~*mۯcjJEe29,wqq*l'9+Hc+C&"eƤ :ݫr]4H#ʐK;0 |>OPf)k YW#$ʹ2s6@;cX=!KbJhjϞoGa#ITHĞZy+NkUe$9Rtm8}oRe tBHk\PXImecTNc"v H'c`h$:RVTny1IwR;P`uK]d$l uꑶ6W퇒,ptYv7峔k/m26:2:9$`@mcֶ;?:3!p]/r!F6Fr4?;'\x۸핱Y4m ?FU#}qqqr}I~tGK Kbɣ2Ca4${JNy 4k$.ASӽi^eޘ. S(?Rxf#gOOם[' zXF_HΥHl$xu3,T%b+dvd0@^ӲA"+! yASӷUE`ێw;/o~Ώ5ˎ횩@,!$a@c>< >d}2(g ;y~888 &l\B5ɧK+zo<7hG)Qҝ{sjNKO?jES.ns4H7NzGl@ sُP #Nȍ@$$IʟDX~i\{xI(%` *%𥂨a#.8888=8?d:' Hu+2ff(*vVVtaJh+>\I ƢJhC*ƄuBFaqޠGK+'+"+fc#{uq(6Zeb@H$n4%%TDUTh<=TK0ʒ, A8R4UT@Ӟqqqq zuYsPXh6E +{g#Z[hb/ZzXJ-. i &gp׫ݪjH\0tu 0e lsͽK^ݩ]<ж^ 4f #G<<,R$F`Aq?Xt ꮠ^ߐTDZx+**C1$o^`hMs鋸פrXa)l-UY/t霬Q΁uWc~>G`]88889S6,>)TMאVxGgRU^)H``G-29viWekQjFX!{O|Bؐ@0GFtAS{C :Tp8>G2Sf=5؉ J7!ƣlhmWd$щ#;SЃ5 >A?% zç:cC3x{,v'URvGvYLI0Z(8;3SMۿ*r$4 Qw TbtSV]9H_[{6"Ag`%U~vFqqqqOW:_8Xr+JۍSbO$\q"(l52ݙb@66Y¨:܅(D*(E<0w*.bMUP6N|~Sګ2:H2AE>g%L:C䈚ex&p=s=j2dIzC=tWdPJGH$rNNl :fGԒd`(>Wk ;^y{sbI?$')_|UƄʅOq_9JA?}PZbZyՑ^9e&CLGU+ANUCDR(*tqqqqr?=ֹ &@`FFOs=3 Ց_m,{$ּ$wxqȮƶ_ joL4M*w$V c{v}rqG~KM4cweh6@4vUtH%?9?cz*#*RH+"0 `%WXwsPX'Y#GCp NxKfh8>?_??^~*KH]=yW(tFhA cz޿nyI]ϫ;j^$lFŎۗn888883)9G+5{CI ߕq~b>qώ9œJYXIUԐF AEW0(,AnQj+M"Fb>C{)Nf^hPHdEb.F I  ?^z9 XGR;rӃf ['yWlqtA0=%*?nu$}9&(.>rhYk@|+*yf螋e)!5bfYHPĝ/ llqqqqq[diON٩:E hwY]\reaIS mqL&)r9V=ef?Vo:cbf,O5dSڿˌגĀ7[.H )N T{vϠ|b;bܵ감{fmBAއPhpZи%0BHB#D3kƏ(=I9Id*mZT"ĞJkZvRFXArоbayy4Fk>9fioXRtm'aiw:}\q(%;,"[{ A֬jMhaq,,F888888㑓R4ך%"qkR.|=׎u 9~~>N8 u;έn2CA* AlrM'Ӽ3@󢥔荬O,2}?xMvc⣗*+'V5bރ`4<>FZW*1ȡ]JAlOMԯ$fc^F ?RO9[MSӝ*%MyԚ.DF0{W>VS6C'-C*Lg= DHliIOJTBI3 l5]鮩ugETVI"H)g""@yԠ|@vX=XrR|rP;㟊ˎ aNfqp+aiZ5z;J=*tS5UNrhX͚`eD@J 0UP?T 2xHhY=ݪ;CHaS8uY&UjWmڥѓz ?Ϥ3|Z꾦} Y6;KcjWpFҀ7888python-messaging-0.5.12/messaging/test/mms-data/SIMPLE.MMS0000664000175000017500000000017111707534031022651 0ustar achiangachiang> Simple message :"<3E03099E.txt>3E03099E.txtThis is a simple MMS message with a single text body part.python-messaging-0.5.12/messaging/test/mms-data/openwave.mms0000664000175000017500000000103611707534031023645 0ustar achiangachiang1067263672+16505550000/TYPE=PLMN112/TYPE=PLMNrubrikapplication/smildapplication/smil" *text/plaincharsetiso-8859-1"rubrikpython-messaging-0.5.12/messaging/test/mms-data/iPhone.mms0000664000175000017500000064202111707534031023250 0ustar achiangachiang1262957356-31337/TYPE=PLMNapplication/smil0.smil,application/smil"0.smil 3LIMG_6807.jpgIMG_6807.jpg"1IMG_6807.jpgJFIFXExifMM*i&C      C  " }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ?Ud㪞*oB@GTP 8 YJϛЇ8*WKR@[#c֤u&\2y)>] +aWaySҴc rrGbk;O и`q!:t*In)ÕuU!ff%Kah?^ԤhIuXzcN8) r ja!WۜEah֤zݏAJU)rj#3f囎zXPO#')=ZЦaeK0 , #?Ɛ( |ܭhg%vܕrqmdHbh*b1؞@K.;޴F2va,߻\gަ14vE< ޽q/QeK٨ڎX¸2#ǎ  j HLzѵV@Uq3󚨾WxtOBDͰI Jx8j6PT7 OE AǮjS7&掁JNriFs:zc}AQSڔ͹ FǚzDrKqx`NBahm Qv$N\Þ"_hWa#Ž(PzT؜}APO͜ :8\28GҜ%$בN5HK݋l`F%vTm=>Wt cK0!ҝ9sFQ ,F:qHVVhA,xHUvʒkj8$&EH'8iV$` bR "ֆ"r lOBXdTĭ!cq='QTy֢RN5҄E琂x:z|$Y|ޖ9_rrǵncsZ+yiݖ t94Ɍq) 2kVv]$= #aݞ$ܣ qz{@<y>/v6`jˠ)qHސCN03^I ӧMY!v}W|IyK c!q{ AĜsɪ\&q^dDeibYQҕH|71ޜ1 3e}${c?!M$qlgLPdV* .@X2 NpOF_,`뷿 jt&u{"PM4%W9{JyP#98qR@v"i5l9OtZ!b8i`NyZSIs6aSt2qT88ʡ dbW jU>^7F2sqRr)f 'ۊw̙_ۊO-LdcsK $dZ+쌛nW[FA`_q֥V+MA ›dgtyWx1L~66*#36 jFvw [7b"eE^bw7jpUt!xrEA8909b|1}R4zH\m8O%ٴ3T8OD/&! mSgI<`SKCϡ"'qiDRpF15qG=!R9ȥ$"ĕrGQhܫpy4F )U 3Ա9.\ c)cSsӚGDj1kAH~*Ʌ tQĥL?LgZ-]*3tԜ&іVCDKg4l_O8&BvGo^B׊BIRGQ)JssOإ69CwJG6_'3ެ\3Rۇ=sda=O2빥olֳ/`щ=:V˗A=RɌ`C֬-lRއ/cl2g#=gil]8W$7cjQy²iUdr%E1Fь_zeE!;[qb[ֽRr z~u? ,g}WD\E [WQ$ ϥR`|ikF2^kŨw>$;` ^rNzpM|MP9pA1\1ONfgkcY8ǦF)@8'R(-IcGVis0J|B}qcǹgj@|2~nVsz~XA:o\ J>}jrف3ٵfDc}2l@ 79E9c$2~aR,;C'O*4lJn̏.Ilzӡ8*?zXԕ>%)nyU ING3!\ cdbBz9R抱\al(.1H4w̗b9Zw[rqRd$y>xH0)s%-GD|!2twPGn=T)gN`?#w]ǢҶ7u'.Y9nz.Oh F3Џu̹gZ@Řn?z\L7m?k|Z|ܧ2WשiTw=$$MU%lpk-f:'& T^[Rm.0sӠFpI" $CFrbRGSH#U8=sіpPp~K{$0| ׵=H+N:wW}1֔O;=drبK+14d"`@XzCTwR;.T*3Nq9H#}:*XǿװR6kH\ ?_p$Ҁ77y/5^Ԏ^8=)bPc$S|#a28 'hJ:p=zsۼlߊL8X7$BKz&dhs.pC!8O(ڤ2 )!t9=&2̪203ϹZk&U#kFʸcޤUGqV-dN1Y`:u`|@.0B3Rќ>\bl6*<e\/I{2P'$rBjXUB?x{jJ2u;jXn2܄lM@6bT(l=鎭\,(AKgT, ¾[T#>b%s=@`?*tm ,y419##i#\y)g,߅\U`{}b֤~ tL~\ u1炇 s*a䅄€3_)pJ`q3lU\J{&о`1cW2v'.Wg߅*fHXj[@P#V-6cJcgs=KH@U#'=N)6A1SJ,n:^`8CT Vf銻)Hrq 0TM屟y`ak%J>2TdV @8Ցbyv7:󎵃QQGGBFт|~ 57.J#y+ϋf ~ھ2|Gd;uKM"s >_hDͬB7@8֪خ{VoO2=0sּYTXZ{I܊ aݖp_jj6$.n=V/uw@X(84Ls`sG*@ '{RwFI>)MIoI6gݨ2->8uvBH= [ON~L*19!R.mRrmHA_=@pkSz*'&teN\ێG>p)NI 68!dzS$ `ђw=21I M(@1?֓`G9M(0I毒.<ԮYE!<:20QOJTL7G<A'95jI%gro(wBGHqy1קO֍\(`RWTpyD|m-F?ZrHdPO 0ђ\qB $m '=eZ+ Yc3RX=)QvJpc94[t*05WtGiOjxaU6iJ1ȫ猯9+ l9av/ b#?,8)F 3zS$p88S z=쓵2q=8ghܤAMV7 ySY0=%'c1f 2:ryf( Mڀ߹mp뚨+zTfO=8Ȥt,:l2@ϭIOH戵}FNQQHS94!jInfPNF20zf89?*"eMKNq&eS-m$dJQ!ƞYi)wF8#f<{NK9ZarקD0,Nl)˝\gz=[F:ӌ8~ A[v%& wU+c=NbpkυW~M=sj(bCQuJe}qҬH,Sr河Z;14grq:翽6i ʝϵxvMIj)hރ]NT˷-qǽ2+*n2n/ 4pw䒍-"L7%H98I>Q4~d5&0JK[w_Wh<͡YY^)]q8>ae%ǷUUp,yo;S**.pA䃑Apl^rwګ[G#LgϧKn.g zsڴZՙK O@<1*5o0f*}’ОwQەVzRh]>ZEJ3#Q5jO4@ gT0^ē)Tc9ӷo'*fȋ_#b~s۞0#B<laJa{+{_1#VP~^)rbԃ$6N{HP*ޞ.pG[OԉY19Ȝ5c8p y[I6gs*#KTOϞF;W/+ Fz]^ H܀8{ҹYħnN9l8pCm tZxb0Ƿ8ܤ$ҎHȮH8s^:#x')bhA؊|A^zfS2IaA3~#@J`)>$eT8BA+$n '֔2;SPN;zї@ҭ(s6ıc8B =0kd3U@s䪒}-R,)#Przm}33pO(,G1C`q&UĜZIۢ2n9IbpWzԋ3n"  ^/~`N9®џ+zv@IknPI늕XO*}GqOKMqPTgIi8#rFX9(Fg[FyorvC ӿZ&%Heَ Z0cҤ I]:BJ9Q3xp9NI9ܼGW2p+/G~(|TQ' zzA.@9 dRr3=$bs֥TtY2Ͽ5$JbO|Z/Da(ZJSzR_E5lL#i9*H \WOʦ/XIIj:l썹nXRm櫉21:noZ)%teQJy3S07e=i#f!p'jA,Jg2Z8c8tb#_'$t\"mh8{Nr3ҍA˫z%i*L ⟜m0܎4DOh'h0qǙͻ>1egT>$ޝj( A>R 1g-w2?FўN=:<r2O<0H$݌cjc}|Z=Ia'\׸ L>(m#d3J'+ܖ\6rr`` ar>_L[C¥R"9z*A4h!mgh3,ѕBzj̱$g`0c94q|~<ɥer>tH vi p:p}蒳JRԏ"8C$av/$u\tcޥI_bRԭ4G{vR励.H }*k$~ TRy#)K}Q빕cA w+_gόȮPVpCxkRA JBw{w8_\nH9=+ψW qݻ 澛A셈H؎duljsy\=0}k A4}~[ MJKtS:I6pH E:sqmөa18#;yd7rO 2c ־ӝ'urb9G=)@ڣ ,%@<bdHT);܏i&EgF =:0) Io:zTDKn砦J/7e| nZ6`c{F- Tr+YTq7ɢ̬`}5b2r94:des* )` (&MՄ L!'6o3ҘdN=g{I@*e # Gl{4ٸt8a@^(1 dGJYPH xҀ'2{9V@A#wB8wuj `,x=f_Sj5dO==?K tY7Z0IJ7C~= *ÓZ_ouiGByDq 0!R*1 58=o1ޑӜ8=Kl@$$Hɿ=?6Omi7;y6c׸{?X5L2HXazSh]~C!{ *wg#xk FyHEW5אs]$;# L~=xu1x /c ^N1ǭkMݭLڻ躼7t>?r9}U@ 7 q W4[bi9ic w}tS8m>‘c 0=pE5IxdHq"hS`s֞]n1RrRRiێc[$3Hٕ gQr1>!{6 =x2rp*5i$|,M56V Gl:EjEPrš.r@$JqV~\]vN=Ӑ21-dDHّޥz3Uute5w̞Đbsi}1^yU%WNN-g.i-' N߹R@' xϙ=jX Q֩~gf>(O@?/~j2"c"3v-ڝ7˪)'nVÇ7ӎ%•R'ޘ"wB 21UYɔy4z\6h3օ+/z)g\ tLGoQu2TܴL%f@{}i A' %  䎹S ?Le9;+_ȓb ####ڥD ZكPw}*rf?O֭I%85gJH ǰ"Wx!{SFw:sޔ 9{Ɗ1IrBF(l9}jtsvPD*"W:qS2I*qսiqkVkqŷ7#GZ8TcӰP3n'VYݫ$t|d`ԈF9D$9SLvM0sǞ)mwn3֣U{S<&}i$cT|QԡP)+)Ǡ42J90ÒF}jWBKifL${dR#d䟘sMKrx!A;XҚ}nSat<H̹9;\pn%&#8L7>m!W'۵raNrNyY d旺kbQRR ;UKӺ\[=8f7ܧvdQ':1\Dq ݒn9`xOr2l䎘ƤctwP?.MtV2rq^uC8*OZW؋wb+cA둚X; < Wey'cإMv'Uhw=Eu ɯM;Fk(ߍ@98wBI`n'Ҹg_O+jm T@,_J'I r{ԄFǡWDKGw#Xɗ|рGl:jgW6m,zwVX"Py,;E.'ʃMI7mM)|W/Fp7u<<ąҧ8J {RZ؉G[D*.2dgu@/#3T$vV*z!snjیN0P]OR=iYq×[E3E'j[vsRyyQ0#m! g'99p]IɐOVJ#ؿ`4nOB:~54~kD @f^֤$1) 7m͌ޕT!>qotmf7S9  w;A%v#1<׻\Bvu%xS|lM_9K:~ әj+T^ka>C77=1Oh̍FHE8cNURPvh|x:yҮVv]iZ=Ÿ JJaA(u 18YNz\z6כT#^>[|9O_#<.#GC Q[]m.>pC"u A^ǃ]/%"|6  =– `WJiX|EhQ+#k(=+6y=:VY/]8=;V 6qֺq9] dmyZ`1'z'%Fh)Qֹeբ$ 8sv󑓊D G9xFz毒#6/zђH ׶*o:Qz8*Х]$#䊐A,2FzqPbx<('ӧ֒F1Q~x;pzDNIjDbbtdb<%2xQlj?wqڕ=Lf*>Q3+&G&ݾSJI{)^0EK"Re9*#o9&n Q[Eʚ$r' .x28 I '=(T5Hy?ΤFnn_ >302r3ZY5r'_2D|LzRm+#Hc"%] \dzְVJ|qqi-pxLf Oztq!]\2ՙ=\>NIsך009ׯZfR%_$1j!'$qSjk`r8jiwTޞTm$Ta:q``NqjӸSI3$)Ðρ'*e$oY(rJRCVeޕϓ {Rт@*א(CI޸4wFZWȃ(c={Zp#Uq֧Xs>Zf,~PsPJ^ho z]RYF'޽e[37ˉ#}JА+@x~X+W%vZTlu^;3zTcn& d8zQcmpwgtƎ WoS(0|4wYI?)l2WfebWc䍹#q_/>UOv;0r@Y*mXW+ޫϧ%Mϑq"$|. `!$z_. 1G[GQ / (oSi,s`i8 [NW_'|0;ZqⲆ.h&UԱ|IM+s&9v$'T ,Mcc8@?(CnL)gl񔢚Tߑ,%[_ybJNU+q<|_]lH=*7| 3-a,v({TҦJ?qrΣO?  %F,>a f!ܰ Ϸ|vFqFqL@+2~RkޤdR5Y^)Lڅ, {l|'r5Ed{2g=ȧ]B4&_w VZV}t *m$j M2떋вsQ|}m&H$p;SqN8X6$Y+*4߅-p߷npsӧ^? .qct5Œ9)INs/RI?B^ γqx+ᔭA}}jX>;*xS<~ #q{f+sH &T%%~㧆s){mK#2*]FǓ)%aв,g>œ ')X'Uz[*+_E>JD+ՙ,D/js~G⼇M2:|axR01{Q`qx\cX} ѡ\5$` gًLm`AhFz_5䮄$A퓟=5o`W<SJ.Uw2R!?0= nY?9?0ANkkmn^.q)`nPߒ\=W#oVS2ӏJ1xl>%Ԓ:\0>L grm6~o%دt xːO"?d,dJ8#=:S`9vJ?SoHm'w/LU8`Wl%q~_Q5szyj_Dw궀 p,gvJ #kjKd̺-F~ZG=fѯ! y7OX~ۧK2֣cyc2 qXl*m{G LLU8i'op$u5$l;jƼ4}Wg@ErAzc"A|y-欯k0m4)K OJVIkt3Y(3`vuHrF|D,~!'j.[B7* ӣq#xkQ?ࣺeʲx%bXO(GLI%u)eoW&2f:[%N i?. wDc|ORۣ#wc x8 sUTyKF9%ny#zەxA'.ၴR@SI`|$Mn"1?dOޜqTK"^e(nCYg5&m2{I afAN0sG_Jw4)ٌ֗idv'IGINg2p9SI{Xo~zEA| oOry|nb+֍G8sGeN|,lj$.u*:}9]b|a!*5o,gv:o?`qDq\Vjs̩iFr)*9mlܜһ$PP2MH#;pH雡Ƿ֓릚R?iN-NpЕ=q{ Kx,8Ԫ9rsNO$gBA[sUW-^MaGzOJGۍ9SH>~*H|d{oٔ8s\,I?ۛ_b #8P~~~fk,،dzf=rNUǚjt10JF"dn c\oĿ 7E$ZO/Dۇa*I_5c*[;==pėzrc1_/ALgRnNxqNoCDZVWoHr(gdɖ~v?;י111p3Rjp)^yVzsֵv|^Ù'HA8uh1Z4(NMZad)iK!RoII2>'^_ Rq'v8(_j1!=J2|8 gSÄ%$`tʿt:͹AFk lv櫕G[_VxS*F}ז7p50NT⥏ eu?"e{y? ;S 8CЃa|$;5`@ ʒz;SpZʜj=QSzs`ݜ⼫+AU.us!4=.0ZFphqM4Lg9p+ᢴXwcY8!=h n#rA=Z/vg*z$zA954s&A-#=?4BíF>/+H-Z`@*V" %,q⥇c+yn:^Hhm)Bͮ[=>O֦ы]nє ?++]K%џ70+9tF ^IoCu>{{ԟкI'_F8cGx_=M횒^7㷮㚨 i$W&*cj_, )<׍GI3&!N;gyg8j#\^[`P*@|zۊhi2^[z+c#?U״czt<9WɎrG=cܼqҥ$0˞N+!aPִ~d`sx''m挭a=9RVvg{3L$ {?ZXfb{6&wp?ߴt"ű̀5W<-+vNڞ 8ޟy#~W'- .-sSHC$XPu֔duk$mOZܠݱN@s2:qC@cGB-3ꞿ226ɻM׊'We b  +eNpO'5R[Iv ?X_NZR^«kSwaTΠZ O7%>xN?'ރFdVy;8>Z%%َ8jz/ ́9"+#>PshOP@8۵3c _luL 1\6zN1ӏj C>J€:jSs.k"Ϋ/Y!uA%qF>NpP ^ d=}zWⰕ])&*Is t+$LV4-,=h1$s@F1AqCXgҮ>N -b Lc :jOfS*T'] ~7g6W9IǮ)bAiam$kGfBjvUgǥ)\`;qϥ!,f)+DJ ^v Ҟp b~UzҠb I:oC^Oz 6nY| lnGN񃓊$RӨrEh@i$cӽ`,s?Jrq*n X0@ F]''~Q660:J- N?v?'q*KIluN@rw E6ՙ4CQ<'1Pgz_OLP'̺+=ĸ|`sl ihc8)͉$PY ҫb]'hlc 60 þ})_->9$,sRz"yo1۷zR GHG̸ێHRҩK]J!h:c(l#);I$ISHfpc8s֏3c18nǭ* rx%$լ@ hrA)GW%>ƪز cңvxШ`GNc${B>!c)-u6qn&Q9OJT¸4'Sa9Tv ]_~҅];#ԙl?LjI ޅ"%}6ݜӎMp'5Ocɤz rH5Ww\4ʅ 9X@ IG_~)2pO"=LJ:j~^1ѳҜ˔85&1h@>cfJAVSv fZUxeISI81Dkiq:T!UU7 NFF:ƩčH"2OC,®Uryq66ZUo c0=?Rlf,WۧҚ#a֡חFlֈdv'X9@h[8A;8F)[Yp(b$w-W%I O€H5o߫|)]~KD$s }`wa~}7>lhh(#%l֯ %om\3W8Zo=wJIdqnpʸ wɯ(s+Qu;ttv<(Ls뺝k&;s@.rOjQ{r -= J|{S1mu9LvJ G=IeǑ޳"\Sj 2ڑT\InZjn# sfUtG+r TE'>XҒ4$ yq'5JN/mdRӚj@e~|7p:"8o׿4 © Î鬪Xc ;|i7\?͐G;򻲧d&vq3ސ@H#=(r[i$\@RʤQdtcl]^;SUW'>—.@s,)Ivj.9QE< =i%N:}h*<u-)ZDa6Qg=:8 qԓN;U&j*hlvB=M8¶IBFx(*HG8 ┤BMՈ'jG&1_{1,eq۸>>I FdinvsӼaW=}F8]mT޷Bc'jsP x:֥1!D{@rz}*ơgaLts'1S(.zu5"YH=3U*QS8SjF9,4A9} m{QziZ OnwJgqRcp PI?Z )9.2zbF7W#N>|!c;ϥ_$Ԉ@[ G/#: e#h\c>ԛώxPn'JijƨQ" 񁷷ZsFaCMHw.ϗf1=q/jZ⢮dAc8^̑z MlIx*?-|1zUkTRDs4Lq$IrOjCw>?xBǭ+ry(yơw"j7Bu9$c # wv Jy{8ǭ7˄\v5NL-dd7|v|Z4xP|\ʹ% O4O uNC UmHr?YOkˣg8ϑbY%Y#o, ܤj (ݸ`<۵xU1RaJ3Z'fud(=T|DB蚶@'GW~1FHufoJ彫݇4C;>n?6Yye4=T9?fpF=8_*,O;C}1_ Y$56n91}iZvm'vmTT~fui(E#}>x>`A f݈$`П|\Ę\WD7T+c'u>Ec:lՙc$wYp2-؎} `[sҿJńEut}șG#6ƪ5y~O~3T|nV/j㎦d{XUҌ9ni)F#|=Zz%$Ts+ֳ#R7Z֎Iax||AU FNJt>0'\Üߎ~֔0I1քvcYl1{ֿLE z *x.fTb8j~rNc+Yj~c?xgYÎvO9w) A<xF, DB?h*Ǎзqgk~? 8A~a?C} 0| HبQnI=:zu Gu#쒢~3,*vO"~O#FurrO4R^9wO kJXnԅ]a)J0Eѐ|Fk7^ot |qZ_7_"5YW#^"8blNi}>^PK})ODn_ 2#d~Jsv,*Wg|*_ÚS ڐ#,úXqh}xTWsY#jt~` uKR/aGX[?.9p:u)ŀ1#aq~.aۄcZw#3;3=9Q/oRr^Qm=SvćZH89⅓:XWRsWX_Ҟ|1&-FFT*y_ 23c[Sx_P;Fc _ƌhxG#ѓk,\s)QVN䴞,hVZIaidеtG}3S*ǜs>֥ê[DyF)UQ[D_Dn(4a8u{麈 W8PXkƟz6~Oƿ[GQ i >^s߽6_[V89v*rB{V9{ n4ét%g+x6ۥsɄ U}e?VzP՝ - Y|>Ԭѫ`#8~glJJl>unS?">)PpsB8?I,.7cnE~niێy2?}/hhnzZ'sDObcA-?%?nPyq֐WH2,AW< tTWcM>"&AfQGDJA9j>o1-%J9?Dm6'O|N]{P =2,Fx >~_qvJB>51J j!V$R.R493bO#ݍD߰UXUpLך뒽BE~kGG1^) F`V6 8W? ) s!A/%휗׋0}TjWR?̾!Z9A:SZ *xC4'@ndO5Y`haI }BN|n[ T`WGV,d ʮŸM~ `WG H<9>O|#m6yUs'nQ,>:];tNۉp c='Slow?X`?ZN:fJ +O /afާƹy)ͯŷ}C=J˴!=ZPqkB_7[G,˅kR^ WS+mM.xlʦ,Bd sֳtGw1sv]hsM(,{g{[Pl; l7{j"<9~х3r9fVk?4`zrkP`*_ŃxOP@P &SGN 4lg윦qҁ8FXZ9:V|e׍Ԡa}*AxP)}3ï _CK7Ui,CxY#[hKn> =ʆe9J1['+PsjI_z+QVn27M<z1K'x=zVm8KeL>`>x/pLX 1_2.]eUZ/\ۛw>n'W$_V\a4kbJG*~sgmV1;^3ֳNk႒H^Yb9c>(M2&J޾ꡌ RϔMɍ2<},v.}:&Tg9NWKA5srThz), gpmk<04Fd~~L?wY Gz >X]@طo30$y0z44xP28 ީbz]|rڕ"Fbt93~̞UN;W}ӎ3Uc auMLb[le s凾 r'8j(36_Sd,R'|֑e 71v'pך-=d\|aSe>,тl$dg,i,x]SYNMc#qBwbyz.$ ~5~ɾMۭnxn9ZNLp\^B׸Jّ\ AnQ+`{ta|0 n2޹?B+,bvN#'hS/N>`WӭfO n׎{zS7PV'RXm,H4xEUuk?c`$G+c6O͆GC3g&a(YWCgOz.9j.y^+ah- qܖ O)0sq;rDvT"T%'Ϛ[j9X} Ov]jMG~4UuI8aNF;qGvJ;8*(I<5i Fݎ{WiSS +ӊl-58l(,e5I5ݦpK,brcOAm!/UG,ሠxYv.r \nk}VRѳޜI޽տbѐ<''G<%Yu~)lܘad1y_!<T\~7d9>1V$xWH ~e $x̥z sJTׁp+[*$ڶʟ-?ȦIkIV`Gfd_$&``iV5 <,Uv&oث0jq8i؜õ{H"P[y4<{H'H+8aD3i3?Z73HGiY^\ş%jPJ*2[!^OJOI(Vјg"L?gfl׏%Wf;cVvP. pp?Jmbr"'1LR!md /|m$$~KPԣ鸽b[cGѤ\dgj֭C'N^tZ!E;cZ+9;|ijI|Sǹ9֣vptc+JITLm t*7zy3'v?kl\qJĀZx$ mƤ6 ,VZUG)wē@F[xl/׶3g;}йb^D>\Rgҟ׈xK^OFy׈W𞷟R Ju齝4i}jc|)Ǧ<52|)ӗoQNHpHR@ݓ8oI'8=cIxSZ}<_wӿ`3?R**rwE}3ɥG> º; .α(+;ٕJ\MhٗydSa[X^9wӏ|B<1GզV^M0~\#4GPv ?X~ rG#<o x$1GEh騹JjQJ쭱ʧ#mz?k3 t~T#:9>zTwDzV<;#(Q* MyW kxNNo kuJPp/iF_p@ّGv \ Vql1TmxImA㝙Щrإ>FA{,c{XZ"(#UD'5'#a~ ζfA}RV/ofRpOCW1#Xq9毷=Q9yhIǩT)W4v{a1˽7&/jEoXb0w/5Bvsg{ҳaxv[6 t>Xd@ʰ1.Þ#nծ?f%Oxb۽U O1ȵvfij}ѴxsV愶ȓv;Nmy9dݗ'l2΍ w!8*9tǣg5*2o]LVݑ$A'x_ölDKJڗN+uϛW^{ڷT˦bl w$|M@IqkatbF>kSntRKѸj5(W0UgyEu屒gY؝zg/d?>f*k Qj)cz}Í&'`۹H J =o2/+Ϲ bw|黶3ڢ_Zlͷ+,wqèV*^2SȨM^U9RCzs֒ xw$1Sr\⽪IY=+gItm䏜קJ|`>->YƲKp{+ JLJй翵BlŊ HBYz)=k?k%;`FM^^ 9\8Uq{>g؍bs-q*$9[ \`''< l6`|"$کem>sF{֗m䜴 8˟q|44;,{A YݑB85|{oY*%{;@r8#> ^B|Ίٍ(Vnz/Լ-'PRߧJeÅ;VW'qW)Ga|qj]TMM#  =iߑ3OI4:^GҬ@2}U=@}.j|8[z߈4]X24%Puc=j%a͂F\&5U_+TucBmm X;l*ik?i&y=H+Vqb[p튲m0eDIn*t'sQM4}#L̿iѬ#XnT="`]-+jq1ǭuV7Vzi4 RM[7xKд襎F YIՆpH=!m2x4/Dc[AkUֻt-tR?48uq`oՊ:g:6,qg'-nȪ:"yt U#`NO$fSb%+257)4 V޿yΛ] \؈ʱP-N3c/wheI@Oj{İS*5|i-d55)Ԏ?)Kb qdwî.I+ֻ~x$mKL4ɾD*DhP_F 8U_M<0rL &XwTx@mss'Ÿ藾Cht\lE;8]἖׶xU¬s7AQhzC!ΦP-4pM!j\ÚP3 ާ*u]?- G|iׅiPш]x'ZV!58D;) p`P-RrZ)<<,39xStw1'u*qWzLJA86IӪ*Ql_wi;cA%s!ߧ1cFzrk>v`,pG3L>&K`'v%SQnO (e['o#N,Y=kO Nck|tK")cl]y+ŽHE5com=bI5|K5;t%_NոlP6m0s~߅W-y-SK{hqgbD7ɛ}Q~(/Ԝ+0ȎU5% q9=3MxHC}sg&O;S8TMw28[>| ЁK,K~ z?(; $T?% ,83i[S =|Ŀ7 }8[`$~q)v1ңR;xyWGcF [smmԜ t|jYV^֒!IBN]ߏj4չ]z=n!`x~5}3־FjxٺVBy>'5a{cB/x9lŸڔg(N껟`MwxZ'ڙF}K۫Úl/,T5\^}qgj:d4{ |Bgۥl׏J2pvD,\BX39`?kBIؖ'$!Ϻy:Z~MKm'P=`_IwyP ‰\7ONU.i7lFrF)vɛo]::MY|tn]#Q\\ӌ[Gx ־eqo3xTҮXF?^{~ ,/~wE;V6.8+ވSdI$, :M!:?۳O\/8 ϩ5߶Í6iյ Kt{ɏNڒwȍMJa6|/H!)FL/ߍL:M89?niĦ,:A^>e}E!59|DҮ 68T۫ᾗ Q^G@ͥNsOQUc Y;zsHʺs:=ʴc Ӛ*OέfXar0FF~.u<Ѧ1jk֒D%VWݴ4^vMoHnp6:c_{|;M)i}ˤAP9#qޙ7͸SrC"iM%OUoF>Kt>nnn.9%Nj?ᶭ/[#CۓJsPQI (yQwԒ|ҮHzIJpھ_(h_/E䎟N;t># [!^>nZތiܟHNq`|Gy8R)tmD0;CUΦ|풊:\7:ia4ي Af_)wЎ˧P=sN)?G4#6Bwj~ _ .y:N6*r w x)"ncxa{T瞿zE }Zin}k2ismN$|߭{Tvsu*J.9alC:{үJﴶ&p1ۭ| ^S|=₧H?˟/^*Ox'}+| V>' nxu&ڝE+ |2A~w=Sk>_Ȳ9 |3bXK;r?^ J-YOtEǵT׾4Em!m5{B?n?t@8ex%rc𯊘3 x[M.VSwfr)X jZoˉd倰Cb{r=무ܰ(`Rx?_p^*\ 1"3x=bwBC2>5SM'jRr|F#tqjkI<׹o5v y+si; scZ'G|P@wARxf3OS<Ux::vm*$rŷ67*/LG=GJW 1 }^&q}lϼr3qR/'F\K]<% G Dp}QvG|NI'|T~U#%n~SI/ȅS^mw&AiyB6ߡζhREE^I-T_?kf; |Mw[XOA Ē0 ~S nP|vV7x&SSs4B0+mR}2Mk6hQsZW{xC=ΠE-źʤl9n}+)Et >dܽ%qֹgo"RK6k^b@X褒*(v}O^*J:_iwV1ǩٖO1laT@ 9q_f'~lc|H FkIaR~Pqj /ᷕB_ubǔ^ZRܔ&Tbs=LMN|$ Σ/O=)s^8#ϯ$!rD~NNk>o'Í=&kKZ2#׈D p~.>eɚ^W{vG-YY%nws*> `eHnsz7ApO| )c^75*9͉֣@J>[d0y!sG֩׷hi%N$B+ Myo%|3KesY@69Lj_pO{)8^XX3%k|Bӯ|5Hwp(ۆ@9%kЯ#ײGj6!S($tS xࠣqR=ͯ8lfatYdg0={3$M&H.ry/zM黔TNqoʹ'ίws-,($K(Ox54Ucoʄg{_'*ZGA{Dn5=c~W  # kg ġWNs57`pG8RĨN;-|bM_Z}Y鯮- *`})08ƕG:I44%m\^ oFҿcӚycmo[K17.s=kuXtk٧¦zzsSGM泺F+r,. ;j"N̒Ȗʐc,'b8n;ԛV$ۮg` F;z #oI2vONߝ^ν=qYZG kzכ'F!JkCsNS^q:'*CӚFӚ뷛.T)ڹ'_C[>ixm ?i`g GZהdshCa4d#!y7 dcS[J=+5`z^Kq^;6v,ɲ!~۫Iazc20kr ✱NM:4ܽMn]S_3΋`ڱ00t`;!ڛ +>'Iӓ?iMz\1YNR1On|/-b:Kk@ԱeVqK2ש7\xQ$_gܤ"u2jƷue4xhAXۜHa2}<+ӧTG3Wറ2m g\੽SZ b@ ;MO 3_54=Q9eIOJgRjZmdM˳f@|CZO?qJ|Lа M"ʾNj<*-nWG@ ZCcn8;zT,lZʜ:PNor OXz,Oz}Ex])P^ ] 4;z}j5?gNutS!cE93D4g9zs^"G{֙ʒ@LZ6OFt?7M:!uC ^ "p959w7SYkT{Y H$?OjrHpCjq&n۩CZsd-͓5!Fq[@:ިj)G/"T#=u5p|ԭΜtAknAG\U$*M<)اpN2GŰj gҍ$1<:sYFwי ΡiZov MOiG[8׉t;6Jp혃osG ;5_-j[iYu3I8)^%z֗'֛z:Fmeys-B2r>~>jI=Ҝ4\׌|2Ouy)vn-#8jua7u }\[p ?W+My'iqgiIn'2`"p8 4ˍ/FkOl#(20FȐ@ cv;Ը6F?`uЁ`bf(A'|Gu@m9ɬNi!F:]hhxXף,wvw7pK).Ŷ5 RO /Cr$ ;zOar,,ǡ@G8&udž2ޘmCik)]Ct`w5K "sF)PMaI_lJqodB3o 8)iAJItW~4?^$e6vpw'9GU|p 1W-u_ "޴P!ٷ;to,^Nm%qO8RO|O/on}2+5>RQbP8<*~Tys N?ZN;nt{$yIYcE۴?SRx>*~ ؤ1= 3=˪1|+@noP7,K(|+gA;#@3G9?s$*1]YRyU惩Iaw_ko1Hesr?:? U[WBԝж-Wl $2={d5 RZ\|Դ,+) GbCqҫK09F[#(.Xh叵3FJC)/Ò;&E% JwlO\wdl`s $zv=%7 Sl$3{ҙH#<:U9rЛ$I .'iGٻڅf`@''杍Q#zRi;'BYbS,zRu#'0vbF=(By\r(\0ouwN@ {[t rsHlL稧7ԥ&%y;W<@ݓWcTp |*)Z$h,NF)V]ÌoJl>\)Cs=k%{haN 3,_nTELypqg.z83zzTa@^˞C}N!K!l|L |ߟ!pOG8KTq'xFrir;>a] yEz!3 k{|W6QZ}L)h$L*t"eϟ?x&b/0PO3\ j¯7-K;mRv C/"Ќdpխo-_wC+5ҰI rzcᚻ4Rt*<^nmc* O)uiujv8R]5I+k7ItKI I!μb(+69 5h]rnG~^YMgpKq qrmA(&>~d|w`F2zRxG51Fnf'z }dF$ E{ں4>9EEy=}<~rs4Gu݃Z]PKKKMv¬3kϒ. @q޻V] a\g87@?Z㮗oN1ij:ؘ+Vmhؕ8uֲb};ZILRA#1 B)ڟZY" ccy]t?|8ۍC"r>l޹[O.l5bS1BNqljh,t_Q_fYrSOտǁ#"-䂈[hcзZX1ऎ)f%’x{ZToOQuV5PMMGuGҭ{AU ("6vʪc8=ɪxzI]D|?k%x HE?O M.Y$F{peGN}~(VӴNhǖ70䃒0;XpE. -# 'S(9b(H@' *冧*OKOF'@di/*@B{:Sa1[ `#`ˀ=AXia%2Āvasـ}| VL^)8DJqY:TjRSnm p|㸽#YdRs֐'R&vv\A|*8uY:H c\nco׿je}a ;(p݃sҾ_ Xю2=sgX|Anoc,rqRi_9cvR8UUvk|2-i4s=(A(I=k4+l{h.omW'O~{<N ޵ jw^#e u(w)hAܤsҥkI3$%`yJA&5g)z~q^6) )`>QϨ}*^/ЕZWrr|FPSUeG/p:j9W7TNJb&C̛]P?\ r?˲MVܐ?]OWsMhFwdX*Җ<~+vǎ]7frrޟZX {MF=Ma"~eͤ/PU܏Σڜy}ef{aY@;QرQ9J̧YmQl'Z ^}=?7ꅜ(n}8=i*L<4V2Ud2|֣#X799szl۠;)׼inH#~ vn?ިTi w~٦ [#`#: $Ï<"潑I~\%Zğmvx+p:TI Z"]6Hk#'籦v>)n5vaxnSͦ4|dwL{k^,u4i\)k>/h+;1O$B` x`} hxu]r5WQ`wB`r6Fn{Z8]E[|#oZ-/uD66!3v2{ֲ68{)u"ƾHvgzdMfYZ].-20<H.ItQ?l,$IxaaӄSMipW@7x]\ݰl~8v׶ ZܛK*DR8Āa9?wklW7M W) ZmegC]5["x.VBy2B!1gu7ͪihqO;=b ,zj1>Q;>b?cBo Acm᷹ggQ߮OkrA-CL VR0[h8V5W_M{>-u &doks+XAJ<9FO' jKτ~ `Il[RH'Ya$tlόց%l(;D眞{k𗇼Qby֧aZ֍=q IQqo!&jא0VM+w4MGN׋MAI52.>3Z8ub=fTgCL[6&K60ymXdP]? T (љELhH䎸V7XA(~y :ρSñfeFV/ Xb0yJ贸s=ޑ榪^X+#o;Wҹ6xRKM2d<@8# fHwT5]xe$HU4-4r21 *|?>BXotxwYpK^)%]C.mt[JfXKX-9i-]}߁l\;[-F##+4HL`mY2w?j[W 4e?xNH9ֻ]ᧈKh]KD\Caëdsx#N4/mY'ψ$<79iZ'ɹ|}J0̃q)sވѨ=%v x~{rQZVG>Z(!h\qzmd]VVj|c&-i仾d$8q޵tƾ~P|s<4y-ʃ܀ дԼc6;,L$I0铎*O]xV]2m c_T 1;=q=aq|?XT qK[=usui#dS,Ly)x׏ҙcZFK.rm1BU j~i_xZOrI=T@.oGG5_+]o-d[> \F;x<)`1iJk%c$d|esf_ gWk.\M^Xն'*{gڽ'TOCŗͤ,W12oٵpI˦.ҜcIݒ׿Zai%VIc(RJ\f^4vN[r#1x&(ʍkD|Ԡgqd>Ճn d|g9zPm'R̛] ^299UTRJJF<s3Jޥ՘c)Z#I=NMqsҜ#8Jiv7vD|1/VGmKP `rx&ry].7kD/cT98 s[F~}0銤iFzy!OLT^9Q?Zl'\>r OV#ն@NqCJ3֝ ېE yn%6F CgI䴅wy8xJ!#{Uܪpi0;IG㚑4] u4 =qQp 0n8 tQ06ނWBtve"EҘ@P:)ʥ4#0=G*Z4ӹY sL@$vҙ#NrkQ9Mh-ZǵAqӎ' ʒW4#HXTWGR^gvsb <΢ #Q+MIn 9簮ϝR}2Ά,@$#pi͵ISLcV@w ukH;f1kNsWu= lK#2V8 1򦢢mCK4 Mw6yywq21̈x9V8GG+XkFȒgLEMVxkRa?i"J7$,Y r=+ZMw҆O8*=iz[^z lVʗ~T,F_ |y3U߈m`е H@〭}p+zcA 5PIjot9e.xkhH ip9 6ޠ~|Bƻq]_RLq]5x֝<fۻ<T-#j\A F0?{ՋMk_jkMOe./FbҽU!; c/ ~NIZ**?pXxEwzv87ߎ[HLW1DdeXAPGF9Sz7 +HY%xN+k%k_[YYN&1-Mn!zq $ԞM%RyhrЎjAp2ICaGh)&~:VK{IgtH iKj< rY8Xѕg # zy*;LD)ʜs8b-<?<1 d"WibR˒/=qT(WCՉb/";݀:dLWrTsLo' gԌyUJ ;X,0y"S>ϊvZ$[/-# дNc ~lMXX36mȁݾۆ )Pgq֦nVL:6@a 6'ϸ(nV?XomY"*r@=$Zpi]J*3Pӽi>Le mwJҝSEg,.8Y,F+Ur~\F>:ryyl{<:tψְOv@`lP6sMJ\ͻ`y~$ZR#[Zj"Љ]2Nk#%?_Inm#hhғ`y-~$gri=Loq/P6伪c@һ3fe%Ҧ\퐌NǥJC*xz4z<;3Ζy }X:?9% bBrϣiܻu}k81Є|Wíz_ߤY\(/p6wZ%9k?<7}/m:ftR7b=@^ʺ[EsjmwHʹ`0O#3>*֣%l&+.3*xknRI}9a[tp0 Wk}y :Z[pd2 2%s 3%D6񢲕ń`Όk_cfӕ's9c5y/] Ÿ{sO1)8?5Y[^ҵsӤ&ߠ?#yj`MǑZ<^|i-MJ9Kitwpf\0d}/͑5#')[ lRc=]|O8@ uĖvV<&3jf= c*/VɺZIy>2yY"UOU'9+c^Wzv{-@W]zQRy-oH>g?5]SkxO6E'K&ɟWTr ? "*?D&Fl?ʕPhz )5|̻)?e8y%|~zsTw1ҟ4?|S"2H<\/>9Ñ[|nG~#O;wsW=M`!/?|,ee,C5'B 8$m&"&/υp648%?-`TDx~0G2cKkHO|~-H<^K uAuNҧ]uQψWVM0ԧY40vp|x&tş⩶֥O3VWWn5}S {Lsso)qu 3/ًmS<?*?dm>ݲ:d8_]2b]KRfn9Xb[DqsYOaRGL_ D? |eN+ɩ5\FIk⼂6ȿXjV[ӑ]Z}FFT$8xjD?JmQ?q޺3yJ~V hhv9q=-w!-2\鐍jVO5/bO,_Le'c8j:yeY_2;G]-gV1y#x%ƀU^Oi^`Sw,xp+t o![[H1d?J$GЮ1Iqt~b 8^b h\cc^}OkǺQ VN K)ڣ|Ikh'H 'x=ݱF27,2%'W鶲6pAlD ,*>λ%4%4txOÝzPӊqϗlz]ߛ})H'9XuQMU\zCHب dzZ0#rGz,^0pA8hTpmi4s^9}Is+-؀ :G-[qV튧 \:R|`1#^2M`$p:߲iW] G4l:O-c'fܰt5-ܱ^z:7A{`/왌|X 5LmcKY,@[N*iX'$z=kEhi훻cgSW?ٶQCz>ttYMٙ99[v bck3`Oy+wGKWiݿv.IA'w!%u{omlaj+l\O:M9s~3T6C2I #d0+IM#|g2;m:K*hyưIFWM*St i/K^uiU!`qڣ ּ=gGu~qGv>B_xz?j[߂:wHi%y#D>|cM ЫgO.{4^-3=^i67zbge!a֦O\ +YЉ~b%~3W$9)r۾¢Qr:LZ+M(܊w[>b.kK sf~c1H@/*FU$j,ymh6X\j 4m&A$d=OZ>F6=[JLJ=>p9 CZ6?ډ'q}InGْé4HN~>>sYUmjL'I>зGx'?Πdtۭ4?,}(Gq儹Heۍ SZF-;+2u85Y~sL{c s䝛5U\Q`HnVf u#'F:`~+I=I`1ӥ!q׏Zac?6G<Ӝ4uaR\#pA_znyF$ie:TK3A<zg@iܤw$n$w#!yL`7;UA=֑R}Dh҃u~ }=*&l̹Hq~jW$@hs5 ==q@P$;!ra9ޚtH8彪22A8 qL*E FM#N@ ϮqLrH<0K>ON\wU$rq9R֑i3߭)IkɤyEAE,sy1HN89r}2܀NǑ>wTm\/'#5VVa{Ǚas)3d3dPn› pnMf\%}ɞg-ޙ*e6zRF/Mb&KlH ͜Jr[z?]xoChl {qB0Hvte Jӵ8<Ӷ728aW-A^4o޿yQ2ʻW]O#.t cZaou%婞bl$´P_ C kumu= }9/>m,܌9<}-ΗkH [|؃362FM;`γϨ(c 8szQrJU?>$% kwb>U #t0]XZu2oeČz v Y7Yj->K{Vׅ>)i'\=ΖИYVO02QLgZ،Ȳ#lzw&}YԲ:'Ѵ-.X_ָ1۾v8hc,\+I?&?FK-Vtܴ_fUڠ+aONk}s_P4iunUb1Ƥ2qvF5S>%D,E">ošfi"m zY˗aڣk{C3Jhʺ\ѶV|qv&8ݖ^&|wQԼg^"ű]72~z-/b^gsx`!8n~r}QuL5Xc@H^LMwjQed3ũ/3|7,㟋K/Ma6wlFK\^I]Ιa“'3;1{@fnWkc95DLYA1ìk=VH3~V4He97oy~&AsF_URsuECO2fܜ'&_@0.q{*MH&h frXjh~;/.t #d;c+f0SoG5<\T~x"=f2F5qa/@Im Wgl*?{E|dn5{x&Lr k##F8ֳn| %nVBA^3yaRigrʰp^~?tos` Am(1g2 W[_SPPb+_ 6mJ1Q3Qkq`,~ã| nCXK6NRRHҮC,x"8՛Ӻ>}]W>ZKsrgO!21$ʥg?[&G]<~> |?#8}_;€lmǯܯS6mI# HMĨL0)<Yi湽gD8]{( x|(X:)XEJ`xj@'ٛ'u1Eq[qχn5NZOC1_ŕ̅F?r#͢_fq8gl} ՛GWÞ @%|qz^-olҝ^ixz #ưxZvX:ו~':7uH{UݷRׄgأOWONJ'7~wz_a}" jX!1.9 t feot'|Ƣ5k̿M?CmW2Œ@ ~P>~mt ?ߧRC[93?JiWA;a*VWo8E5$`Fyb5pJ6zff LlnG^N_-:TtOޏY{%(&$HJNwTO<([>v 8-IƘq{I$o^ qwo(=I2E/e)}n?i愹L|׫ȭwv!)>[F UjYMJ"yE4-KN#wk68~NS#uӟV"_FZ˦*.RVoG٢9=ݧ#HՋjj:}b#; |8 vs~j6#9C'eyv:ڌ̡όoxNr'?Q?8 zr5W @=ң=t؏g7eL}骙% { *ğzU:\'AFe@0mz0cְpZ͕+Xٖ dQdp[PkY0~`>F, 81"-{rF^=`ѻi Bia iN3ٍS{u?.kx s92e܌h98柦xXd3hffHOC={]ʌ4rq/)4jF΅RU`7+ ݜӼ9xP&G"pwm̖ۋ݂0OCR߇X;dSjQ($FpT5oktv)tMͦ 4FNHfӻӋG R,w$`۾.ܹߓ5>+Z\~Tb-bmaЁð 3VuOGu f?(ycUagp8G^ifou{_K,ζL1 )E;2](N\MP`HDռ429u*=G6+CҎ.c*ZVydCϹ/ε-2^R:EŒ0{Ҳ/#kn6u$ ?3q}iƔb姩vmoǟ(_}PA%=;87'^ Mߛl p?x ~9ͮ+ǨhmM3IfF U?0ؖH '" N}^3#翡Jϑeڏ4|UxKq*O#kIx"KU_{xV|GCPXl. )l4B7+&N8eH֬O*\jp8T*y,bm#!IyqӮ+_Pg۹$D=¼(Y7zWZ?kOW#^Ӄ0_X%!a%zO,[[lIƣ?QT>0Նȯ$_fr9$([SHB#s]ZR;i]05pJ;RmMٜM,&??[04O8 Rq74/RM*O81Zt2{@K/?OsxZ 򴨜`8ƾ#e-( )G9 wP]]MkОg.;8A]5(Т[m r*I%mouѽb 0^#sh涸BvHH#Q]`.z* A$"H#2oswcp,R.5%qᦕޗ ݮ$H;̖&N>ՙu|AGMOR>|am{Z=+Z,q'Q4Xۑ%#ֵF"{NM8HmW'fٸ&$_i1tPz/ː-Y%5j7W&O9&6?j#y6s<$롤Iu/[v@?/SƏh-@KlIwn˚C=r^N Q𤣮_mn;c^?JUT5Ž9FGQWa:yS[muAST+uE g@nr0k aˏ<P? mdc"~F6!IF4`s'噹2sֹ,VgsS> ҹ X{MF(miѴ*npÃ^G>\E) p\?ZchȥocSdQGC,G>kBwJ?ưΗl[xWcRV)כTDo6$APM'+qYo<($dE {,s2H oe}kf^'+iIj7jՊ|kSHVRŀr}j-Qv| N8'5B $sԝbχ?}7~@Y@WϱJS X5a@~Y$*}s' JkHpv7#?ιx2ҬK (4 Fsu MeUHd̓kmc]tU$g?xMְ\T΍h'?΂juͮlVm*/Q&'!.(h S+` sjJ۟L?7V~+)Bxxͣ¯֫Gv C p3vZ8~2?@O 4|Nׯf;]:Ԇ ryOIahQ@8 J6>/r3UT2=ᘝ7V9<~`s>jJޫAƽ9#hiͶ$Tmذ#0+ $Xj`֟'ԦԐdu')+\jE?#NMNM=t1;&$[~p`1A~ nsL.,bނ8Xy4E^φR>l񍡔X^“[Cywۥ754I4]i%''D|-7 eh `F*7Icy?JI|ys D'i{.I)alBσ49ǥRJOz2OC 4;}L }~ZJzrLSx#HCa ܏ΣT[2c=M_iN|~1j6?0;VnRwiG+8v'OƧ]GݕL"0ʷ0`p .ёԏw7~:q5ź"% Up1Lݫ>;ԭuh\;xI&!W{ώIkzouŕO$FIP00ǂ[EISHʯ"Zky7K/<#q,iOmnwX)#G}N/4 2l8mX/3OO*K"kM*LX8nJuJMCKwiͽmغ^@1y;xUMR/JxM]m+:W- ۉRc$ I%qwxE6]f-.;b&XeE "!RwH U$v45ωzGj,`;\zT5 Ԯl1$jRI3zK͔sE%F yl0v`mC\_5Qi4AP̍v*1cӭzӾ%A)t˻k>D5hQB}nSjf|E<:x^}:(8.U]332U~$D ie;l4¶EJ0;HzF]&/U5$K)'vUHg~T~ѩ ;neOA:1imژ  W|gφ~'vڥϘ52OA8? ztg[I%ƞG:s68+7_UVnt?/'G.m-7\-ųr$,N{qRxM'.JI[Ȳ(OpăڱQtӅxMQy8P^C֫i7BIHoV p^v2ƤUcL{g78矻GFxvZ=Di7d=j{jhMï'(ʓ sךi6emFci߽`ŹO1$Թm\de eK "e:֚"ٍx'Ei+t&7فgM4{aNVژ,wR\ad*îB0-tg;R1~l1RqQo `#? cK'n8$?~$Go`(46a2 Z-U^d{ǝǭ+?v0lHBg9LvJ~@=@ LmْQҔ`8ǭ4<$QQ9|lu,X&J)M ?{T@Ro2t%Upr62 jsi| 4 TuKHg渑\J9HW)m8TlpT wנ%(I4A-%m:Rebg.(DaDj yGt_P$7:doxN]07b ͔y5g[masZ[χ ܎Oo/ ӃSmsMDl֟hSAҐޓWuT=8($^8\6+1+~ IX5LqOL@į:7v3OIw~R?sQ 2{SΣvXo9MDʫ'ѫ=ImR+/& ڿnE"+Xg-%y&8ū]+UcZ}k(S,p LLb[G˨x ɤMF#]`E}TJd*ه#J[}TUB X#7-u gJ{&=y79 GJ2H5a!ܻx_)]"v9 S+qm澹x@%Sx5bJ##('=s]x+p@I%cH-+ko#&,14du?\&d^ /C2Ox0HgL%p ]o`T z8=*T\W(nNmoӡگFزI`<7qS]^k4K,Y, }6w=38PxRPdMp[XFv8f?@v 6rG=XB)FGİدyݼ#,pH?b:x݈_+gĉ;3.=+$qkmJ29 STMt4c%ِJ6iqRU/(2I8j/DxREQ @@8>Z٦z}f1N:sR"}OZ+أT9J^պ7=i aGx(t9qϭBJ+0!@<ԉ+=a?8227ܞ81pG=* cgWۀ3֓ئ009)w#ڤe+m>ַ%Tt$*\ADM. ;zS\7duȩL-@|8= K <`uSZ70~^٦01 rIH0yF3'}=RV GZiBf q HRǜi<;wsy7$֥rhW#x@xf$቙m*wC J6{ P۶Qr}Aޢ}*؂ilst\`cTANI`sңIֹF}O Rj<38Q&g#ҴV,v{m<'9Jir'Yu2dvݦ?/қXbD2V$\qҕA+ɫ#8\~G൯ v+幒#ô´##W-Okw:AkU}i6&4NP7\Er98ƓW'cs WOR?[ |;jϵytJ"†V~oҥY\HSlLŀ8$qΞDm;_q(kvk 4gHZή[]Q qTZShV6gFf1X\ktm&_4KHs$=jޟcRI4#R,lH5j&q=ce\ +OU1 >RF}FO5|mծ汷{kp7om9=+!~h$-`QijX>| =~q~:ΈOg%K*6|pO$㷽(EEO^oM7^~pbj)IfbĻt>)M 5k!#̐1ĞFH)u/ZgCIs/P^MIm70;1k[I{A7=<UW!FxϧzW+H-vȈ1p;dZֱڅsEĎ$ m9kSL|+:i#r#3]$g88;BP⛒F*B)32Sc~uZz.q=6b>{s]%xBO5==ňtfp0$|`[3xb} |pzӮ.ĶPNRrpx^7/ZIwu/m%GGshp)+d.3\YexgĬWv4TyB!x+ wo"O%~ 'ripkxs]` juCZlǑ;BvkP$WK݂EW$2|#v1ۚf)稥S`9]+icl#8j'=NT"%mɒ0a{iHLy43M QjKD9 NyJ́A)zUn3ajbNZ*-~7RA>yx >Pdr(MnAwIPKu *<#1I8=';t\3bj2\ko2cHHUܹ9VXsJ q} JfT()su)$ǫJr< Jʞ}Z9qEtKo$d֑ -֣Y" aop#qpݿƛ#t &yH~NIa4Ny` ;njv֘˸qۀG6Dۄmnj3ye~XQ)|2;?g㿆zIrOhΝeUԥ;.f^4S= 4xcW𷂅6Pr=HҿFbL1_XIƱpU7'%=[_6윢#8)&8={([>a9*x1M9WXj ݬױ$Q-㰯IL/ r1{=+# WkʰĬ3n=nW#ʮ=ɐSLaI֝tRȻԳVF󋕇ۡcy'8?r0]~ɩ?N?|8'+$tk?爕)j j≮h~Y.V#InTI/!t FS֢R[$8F̀o=L}j6nKQ[8Q)G5+}I ;w٫'#np:tLOӚr̤* Y Sޥg jò'MrzuIțYÜ`cHN<՗#h Fzڛ$C|xXr(=Jf8<:ҋ Ӟ`˸v֛Q3Ҕ̥'dLޢ2!%z*ؓs@@8 r35|%ކRO(JfV Ǧ(ʃCNxhSWwggx4xVi5+ۛا ] 3b ^ ESMj%L |tՍ y_`Fw07 :߆Czym-3#zҦ+;SP5 '~kî].Ic`dp;A;F,,kkuco,rJŒ.$ 17`1<_xw i(dXL8QG a_Q"eŽpI_>0nI6Zg;hLDggi'A]m?ÚxigxR 7t1ڹj?ā|,)H7ʂ1qu|QgT+uK!smqRioВ}Ӭ){MViHe $Qy8K|pMgUhM'sIsgXCᶲ>mu$k/mtJ.@ '$Sg.t*85p% []J8VrN~n5tNK9/wiRKm2S,Z,n#s*y*z]?߈׺n#M*ey/N3ֹ yu6[T7 %܋]H`pk|/ /-a1+;Q˪z.Ku2kQ1ikc1ClxAFh=ΧvZxaav~l޴Ec\!k;Wg>Gڋ\+{&^oOm^ʈy4o7}*˿_q xYQlHVe =[JI!ԵKx.e7ܲ ~_ϥyמu tXY$fB 1 ?<;&JH/s,v@i<(z=j]c^4];OdV{| @BY bYx֕" ` ѰO9WMOVzkixb5՚vܩ.23Tӻ͕:ЪMS𞧧sR"f]C¶t5ֹK'lh1"n2b/R6^90; Et>"ž6=:yVɒ ./nTҳ3\b~7qka^i.4{@,c$ƻ#CHۤ[u>Tb ?&;~af5K׈byc;;T`rCך_ xEp\pF3r{V KQ»S*Ꜯ x㾽hh.8tIB\ԅMY5ż:T6)ySI$.kA8#ޱOZ-+-~)NA \i`?9U$Y1OO?JB Qzw4=2{sB4ރPqҚ 98jHR1jj@I ^qqM\ZO391T%ɭ'plښ09V=Gz à;c¨Mg|қ Lffd4}JOK ]I ߐ?ZaḀOjP0Rj,V cq*5;l:ZyT#J q]q$sPdV##pj9Xt$G#GS;;4G<)_p x_6;Q)=ңH?֓vՓ~JH'ѼgmI!@&U0 28F 8܎8]32iY5՚wȨSJ<)HNQx ⋻k ';1q${sc `G'6~6EB#I3ny$coAWMrX;`^bNBNIU0P}dpO8O30ۀ-4IfUqOM,s0:N8W*oXڃ700O~ub-ǂYCksv(8e$1ec_M.'M%us[@5I8 >fT=)È\AgoҢ20jk\ʤVYC0z?<! S{8,, FO1IPcBڳmhԆ^r =[?JGlОԔ'ȕnIa8p܎qQ(J:sG[rX`SB,q=;7楮ŪIsS^cG;|ʛ:isq:ړ4^9<[{:R1*gҡ4&NTSd‚@򨌛N>hd/؊-َi˶AL[y>sQ^䟟ZjC\'=P&+g<p"iTWAH.KF@ϭ'Pd#cҝԒRBnV zRd-ZI|OJpp-m9fpI8@9=qQ`?JOB՗j댶G98O `NNzv`zUr]"S!|47yOn|OpiRA-ط$z\8`4!v⥫hN`.B*S%;vW+ЍSS3߻r|]al.-8&^=cx7<6/!ȸD0fNregԶw,aeY81u`BÎzwÏudOD;vm%wdq{y"T#)/z3G:RC-ۑsnVBz/VӯN+#j6sqS⦩{ii7VYn#3$z 6P{ToiQX{) 33 LQ%AwvRq}޵.9m.xdz uZ|ci_Ǡ7 Dxr/5=kJ/u4-Zm (g\79ksOo|Vv\iQ uTW'ncVӸKӼ#eBu\<QڒEUˌ뜀*hm~*k463.-19#9i}YgӤ^a."$u⡱u`ޮ$߱KQnc+ ̠^jE+]B6a|B>4L\O'*akӾxē^Im5@+4bEBΊLW8q=?ZQ_Pe79PC>YkZ>ȝtrH3Ug#3?Ț-M뾥~ _iu-Ō d#AӣK{ư K魤ނe)e$ ~SUOOnoR$+ueTc-߭q>2M׮³,Ŋ*[q#8t_tFMOJηk)\Bp4Sj6(+Ju6&dh#1ʢpxxGƉx[O*Z(%9x⟄4 O5Ke+js83F+[^Zv 'Uۖ ;qȪ\b Mz|=Ez/ U[4@F?*;PO 2Q\э>LįZxE߼/*(U0"QJ2y'G̫a- gO'3m#Xk$P0]Ə 8]Hb2+d"I?E֑zmiyr} P%ݜJ~VRCg X8=>_riE|p=Gla~A LuO@mDOy^ڛmŜ r(To/T`W0I:柜վō[~M ]C2G'598_ Ӡ  jRjUkċ[!` FkDęic=+ԄU}G|_;l2$E_ghd?~D88#GNDR8Z%յ3ܣC 5ZMR$AqpM> Ĝ)d`Gd`E>Wk\>џ^ԧo1j1mݷK cR.{2du{}iIIt? SJ g)DE' Dֱ(On?$6WT/˟sQ o#2_ͫ?P -&og%lha+ڞF*)}fa'+Scf-sW<;m4ĖInc/>4;;Q㺺tp?AE5KOԮӹIaϠi u!YmedgJğdѦeDmF$UR @VړM"U?(횎IU>V+h?uOȥ(7y|bEw>+[~E4`IvYb%nfPP3,(2XK2o,r[5w7GˋˎxܨPuVFs,-︍Issn8!OL¬>G"=G]]:[k(*Im4{UyMji뇻xYSn gT|yax׺MֱWjM̋ * -֝ɩHd-/'ɡ͂x&qί4l0@̳:jм}5KKB qq~e ~zuwW:j$r G5QW=.n HkǏ:$wZlcGn6KEz\p +EuY]Eo|E lPN?fԖE3;ڵ׆m"ZvIr( Xm"%ߌbV1 op:B^I9?Z,xoLXˬdor:0lj:U-(tV&9 9$֭lt)w/f c$n/jI?mc3zq7rj;6 4I}-l*6.~/⵶iEt7. #\|׊֧%zFMqVS睛)F-UE8O[=DJȊr{玵sK0x&d~y$2x(cߍ(ǯ!Go\2qc&[n^ɵ9a!G*o o8SLcڿA_(W-1r}j`Ҩ knT`3 ~Brs6ѷcqD?N+4,M=\kѾXqLל@$s^y烁etaST$)GWgUG|ɁI=rٽZ߿~u?zbhFkqz¾"ۋR.IxJbqnt⤚}2HEZЊ‚H'Zu'y'*+F8pDֻʠ`A%1j>i"<(;Шc< _Ê|vűg=kAmWh$&mC07M pw3eϧZ˹#zVr{1/veHf:KbcV&JN{`O׹]nTHJOZp3`/jOl &Xʑ~nޝ+'JwLX穦XYG<V *aQ p29'Nbpn! ؎wV[)=Lx|ܩR3օ*J?dŧ--\,n">n2zo?x_`0Ϩ!c숝ԒVsWAo:IM)dLBB0}Mbye㴻,SB26v/^'ۥz1Q[~`hnSCO-'P~u7-I|{d  4Ŋ/+)(A!sHbAqա]Iַ,u"^H| qҩ,ė:$,88%ߖhZI꺷ïͤEmbK*k|  A=OZOGij:pk{CbU%YewQiWeHBF1qV/ˏxoH+I [F!eb I:SQnMtO%Sh˟gxOVZ{Lq%,k k` tiZCigk{]Ap Aު_¿>aoo>}ͽSDѨS`9C${40~"x*zvfE_=Y̛Aυ|-5į^y4EPd%6x 8'<9C> Ԧ߲qܒ̟"Oޭ>},XKf!I? \/ j۾Vۊқ lbSܭ\ԒCEp<ӟPZk3\Y Ilp+*0dM30~tG>As1]9!*Lw?$tjOhmYܷ:b[(Eܯ&HOs(ʣ{Z듖9ZxOzF\px?O5쎀nmH/bs*g(Q?>*c^si!,b4d 0Dg 8h|:񾵢x'tȯ{{1֮&9q3YɦCv;]+5lHmw77_Sr@7!-DZA3ד-)'r%K~'IRt-V2 8 j3,4v@mӾ&s0Rӥj1kt-<9r#%%sڲ.#]w$ە)*,3,sL[}YKUW2^lj>,]2[}>qQY'8ȪwOe b}2 'X z:`;i|iMkS7:$,q +35U|I~US[H$8-[ZQrO"QK6ۡ[W|cn#u5_xš4ψ%nd`Vap[9}a\<'xgvN%,tRCcvd dڬ7w.B-Y0]o @#=zT5uJ-t|jV.]\I*@Zȅv l<\wct]F}ewB$2'wcq]O }6_o$ { 3Jwd8W5?ios W3tb>m?r~crbSNGwDGG"y[׶*1WV|;%ܷf."A: 5Im/Fʳʩ|ČzUNwIOC8 KdJ:ЧJtkRQw],я7s\K:|2ebx?6ڥGRKf$iF܆#E~g^|LZciʫ@W9hϥ~i&MeB͜9N*&pKbi.χ^-OLx<%I~G_kxzvmnmEcL8"!{!.8GvWٸHnuYNԌluuጥi3Pt=(Zdd99?z1~ħ(SjjR6\y a; pӟ-2k#}z(84 2Uy8zkXZk*zXe@T>K!s-¼R)(&f`:N)]?#UFrgO7lӓr1c8UǚuڌuzW= /߳+rGmϧZ<;IEjqhn 2}EC\c ٓԒQov3,QWVu@FyT̪n8\Lm.7iH[kKo_OX4ŧĶ~Rј$# ^,(X9Şv67wDĶe%>Q[x<%hT 韝y͵ˋqef N:TMs-Ӧ#g"t ;8_ xW^ Y/<7-"ehw_ZWOYo[x%(~H%m@zIgOuwa YNT;:Q?[un]85~TIXa~e G\$}:VgKl.8B70+n\j&rDщ!Q/hL3%đAD.;lmcjMͰeC R28kGߖ9ܷg3v]JfmtPxgQ\ YfX 8N] $'בYZͅ"ƋKH9>1X46z\ڬ%c?¸hW3z/ #QjwO@ݎ떩6'AF[ָ_"5رK9'Ԥql۩+tNGu*ݺ3f_b>sxd0$i:T%C|BRdAvAڬ>խo⮥e}CG?o,.H R@h_ -uo%5Rynx2 =r V?'CEŴ peG?Z=ζl'oi"F"$|Vu&AQQq_S7|I{&=јiZF  Fo D`Pj7T2^$q+ Nuۻ6mɜ~G-=s_lP6;_öN2(Rx\sXEk#dF (Fw[Z1u+JtHnv"sK6DSWWF\vK :g{1)ԜWPE?XP%XNV8NP?t9}uI6%e@1Ʊ~,|g)gRMWkY'`LfgDO00#$ pޓ{F*zqOA INFIi.뀽:li ЏcZ .BY((p@G)ezݍJƾٹZ͔Ґ]G<ךI یN՛n)UOmr9})t<?R,GMr Qw4Ey~AR>jτGǩ9݁UOSD^I3zF $ZG}7=zTʵ1.-cOjۻ$>41)*ધnlG~\ )O l3ۑϵ.cUmUliN89 F@=9%(qt+y{T_[_h_,$s)rsg9GLR .S)t38)ZPGπ{Z_ZS9s֒QƵxʬ)Zu =Z̒`H,O֔#[5>T*B݅Vۻ')t'peǥg-r2y9>q /l˅E$hݕsҐn@'AOץFe䃑(mȬ_3&2AP͌{{coe#ߦjalh 1gV:@JVJqH[j\y "zw*HrFGJ`ÓU/Ñ\sL7(39z5$37=Y;ͺLqM0>Efq1q^y?\JU:*ry#kV.-9.T۷,LSGQX?\i@4?:I7|OqIx?8gSt~jqF9=Ms K+y7Hel&1W@ 9mi.O)WsK~H[06vtD9?O k~ YuB;2dXv?9cD|;I{=흲j0܊J^ҴGºMyo t'z|Ww͞*c-^M> 1)$(`qңM;ὖ'G'A!ЌQO=[o0}8SSfIXI#rHpFO7\Gk}Zv4KӾ8)*HU)r/zӧH9AZ :i,WR2E?^-xK$pv.+I푆;cRxǞ-J>].JLX\mf3_ׅ|ki͗NXMhGB's/-<7{-<-AB^>c|V^XQh%f *HÑ5#PGo~j>7MƟ{eVV7Mk!,,x;<k3zw<5xRoYWl:H퓚#>u stF?pv;? ž.ѠM^][vphA%BI\ ִ$SYOo\8 {'ւX2~/~Ξ^-˺'RX9Xc Z>$w7Zq-ճJMAH;nzU]g~ tN VC4!S#l|+[-)hm"\Vht[/Miu=j_Q! n 6;E]gT{ĸI"kdQc`oNYm|b]ndY'WHJ 䚡S 䩮Z]7,&C8)qI0J߳3hgxPүm> Xe pƳ?3?9'] ũj]ΟqAsj[<xx]9td-L%`l1;p02y|Mx,tkkj 5-n!eͫHBv,C7#$Vm=擫[YN,13"G]~rGߴj2UWcO_>[lys kʩ:LLӥNP_|;hQEXX1ϩKkM"RU&aʂ:`kj5k)h%o??Fprm4Qs3~W`co[ozEi 6Y#2GjW!N+aѬ ɸXXcs֬4Pemn׷*bIK~2ŗi'{t9`7BCo \rO=RKUq2wfgm7u8ɷcHsx"i_^)-{;#$lHLnHjْzj:zjbF(FZI:3]Zwqlv?wӴ0YF֞hF /%C!,۲zu+I(j o^1lCb}O=kEn6`py;zSdF(Rݓgd9sGS%M9r'ٓԃyoa 19c 8GaҳUiW`Ij4D,jiI},HU|vq:U7'JB/2H#"wiVڴuSW))lVv9;>bM}]dU;F;,4h~i@^d"3;LǚͼFGm*ok|@*B ~u?(ZvUMi<^xGw-&IX2 <+XJo-v.%ƩXКaU@XVK)c|7Z7R!a]^-8S[2iї 1:r'+?1i;{m^ijE[jm,Oq {cx귍ao1I:p\j?7~Ԛ!L rr\zktUN&9;L|JQncvWobx x9Ȋ2݆0zpfۯ(otU-Y$VERq9ڻe}I/)_7r| :6[[T..&$aC9x漬֍(>C`щޒ9-64ѹ cj즣qI۪gմI_~KE=Skc!HH`Q6kzD{IIUi?i: j <7-("<* U%ϰWIZi1y ?5CFT=4cҨtHOh]rX۞ (_&qԑ9ן+SֹB=+,_a@#8zG/?1^{XŔ[8D5HG}OîNpx-FJWZh%ӐH R|ÞwʤKP7(9'm:A#A1ڕypId8=i vmnW%!w㜎Rc)kٷd?ȠZ0p0r{KVi}wFXs5bCwnA3J-vts09F8ϥI2d Ry PTiZʴc  #[0;B~ΗE׿4+R"PWJKv+8d^)6ڲ1AԱHOND!<r;X8!sߑQwn[rf<{6uy5*S䊵,Dc*5sI #JmztxC|rJ[;d98Ps~l>a.b׼yo f]0 ^8[9!R۶<G {IPqxY7܁,:D>~|CFZA18 yPrgvi1ELH=)<3+Mn-vI9c:;why9nIk bMKռCI>(&qR28O@uYu(heexA>f,$mYEK^'tM7AirAkqckx# o,@$*S&Go \F/2@JgRdYc)jIH4+mJzw/-ϥmxv~EWNItGHVpFHQJ$iX同`1ǹjLrթD[-vo&A/ÂImvw.#QgR08=M0|׽_+[%ea3qt+\ΰ$ptCkNLy$a1er [FWVyuc{=LF lVfWn#C?ľoOg&7Gyԣ6p]߈U(kӕ5|CXR_.D`֨LU$zk2*_&u=kima1uiau-O]c`H;9ZyIuY0'7nJ$sҶ"IϾ'kqJKtv@xP^e=&U##_])lթqMUiF8n~xo>[rGU]I֭kѪj2F"a,>nҽJinWlyFCֽ Ἶ_]QLdw9i0ňjz{RjS[$?cúnsx6Foq 5H`o8a'hŞ!Ԇ oL%# rick7'R_ zc=i5~h6Y}yw_yZa"/ft8|0|Ue -:__I:ܗvؘ\!Vp ڏ@]&z=Bfm u&y.2A|7T:Mݚ 6 \c!!KC^7Ѽ3g+A^j)we P7Đ)wF3N]4>,ph0En]/O丑V(.opvێp*#=2˸jZO1;b>#뚏vu,5u;.G  ݀F9|KjzWruEHC+DTd.IB-Mf$hxS|v[G"DqLst52/Ж;;&hRyabA 88s5^: 7ѳYCrAz2jڜ:vYGopp"#-Ӛn<_[= Go Y^7vÿh Y匧(vw '">T՜ͮS[wW*q8<_"Դ Ů[iΒGUwj0  兮qr|ԵIټgo!YD``W!IMsCk^>]EEy?j$gϚrGsګx{Ꮓu ?dӡuc&I @R994 |; :.\TqWg= 9x(J*F& i^ rkՊ%BM.@B| OE-n#ВkP D>g#n:\f*S%6ȪRzN0 t [[ChF)a@20[vTB<_{2קc5߇V~ FMF^!9dEЇo4߇V:EGec3)P>x7um@Xh ʶ GKu(eZi-,ll"+吃izn.QjMI;iј;.[–Wx$0 6x⧉uJH|MJ[{Z$nƨGe ?x%⟌|Fֲ[_m5% nڅ#g|APVvV_ѫiٰ=0*%(mqʪOWѷz_Nˈ˝ϻio@+KV/uKf$u$ 'Ҽ&XP+Eww3hM-6ih}@ycMBzqxiӤ4f/Wo@yMֵu~7W.2z㊪.f@GQFJ-;J sDK6,hDFe jr:xuL\n!i,>P ¼kȋ#.Hޔ p@z=??:ѶwoXfcOh-6ķ v4 Zʒo2+H0\*0A漢Mn-FKwTFT7*w*kO j=;P̐@-d;IY&$7.X /}0S\CZY229KxMncO_\۩BO7q?Oa\-f.he -: tJh?kE]%Q$og~Mk-Ks 9N>YN=HMp7yݑ (O^5^VjЬЬDY'O?=xVR{B,6ە@8rxe ԌdKTwرk>2]b(5hZInr d`#~#l5+6Q;BYKjF*cnFUGYkSYxô9B?w׆cVuW_# o 7-ewdI[•Josu! >ms`Ã*EDov{*yc<=q֘m1e-Vk9/>2C&yomeRQDbj[_ Sٛ H߽… qx[>w-FC`wS4{ӓZm˶2A?I1Zy#RbΗH\xwFxrNzZ~: 㾇jO:OqhF7*8\֜ ,&ӦQNj蚯_/My#,dnW tGfE-J\͛aj8WH{u_g7SL,MŴwH$U]æOj,Cc܈4Jh{ݑg A[ Ktm'+HUjqzMݾ(] L`X =21w}_1:oO?9x^LKJ;4kQ6^2j;_!Yjk7Mk<9+UXܴ˜P 8j_7[-ͣE]-vkl@1f {T /,5z[Z(3\)|]fZjzO+p[9Yc c#▚XooׯE|x .375t/Ak54I]ٳnl¾-u 0Kx?୐\]9gĎ/$%@gy2HCw_| ԞIeV7Jӊ9{wX]U2Hi$+' ?\ס;klBkd:nI*t<8隙$ ὀQF$z \4$b78ۍ~ aڿ|ّӤ<E~ b/n'zty"8ZU9Vw\CfTPo޹f1dgW0=s±Qj\C-9jET sUgRSiwiRp=JcH3֛|@֚\.Lr3YfXJ_9ry^c@~Or tԵdRrw.Qc0j OLΕ[m Qgлtg9cvwlr3A94IrqJ䱾2@KC؏Z^SJBHb,;q MpA9Bz ٍn;+zPd|> sgH]drڢ=4L>.,P2\6E8ya@4<xǽ5jCމ 2r[Qd}C`~AM41ژ d52d%.Wv9uY\`vnW/nI=>vI;#qҙ$@<Ґx) s$nT;4Z_7@'`HGJMMg{t+$wfW*8<9GV{sKRo4NA'R~b1zTd7+X*TӚh/I']Ok [6gF LE>>f ӊ5%LJ5.vL!&U<$6BAqP+z9 `=` ф̀o'\U>td6w~>.nZ`]hر( ,~=Wx^۶:,d]$~tH(±>Px#/me%Ήa3OE0Db)җŖ9{[_VV}E#%! @FN$ryUjסS0}LjWdfvS;~S`=Ȫ> o&_Z[_...FvRdm]&q+=ݬ0o!$sEoYjqW'|H@7d$v 4!IԿf. 5[HS<2H6~@.|cupj(2<@2܌+6|4]IA🙥FH^jΑ=Fn/-'dEEeHpBW#;g'ovvQJ}'}2z֣^Ȩap0w{W|-4;}o@Vu`VG g `߭C'ndw:}`@QAbSߊ_ѴV=C {}?Ri`9cXR VVk͒is$SŞ%ƥhiM`$+r8#/V?sqK)$G5p򌫻,1g9]|{uYE{*@qOq$88渽guujgD*P i;K^qig|Rt 6?e7-An0a;z2[=xWÚ_Vx>j̘c=OJϖiβzO/jMť̖֗6 9l㦏8<Zne͈{5&-+Cզy?duaq؃֖3Ǣ3ۃp e<|8]4$!3iF,F0`OJl]M2J'P-u3 oh!0mI<]-(_[:':#R.{S<ǩobF3}qbkKPF'>\?5?aG5Kw6VU0q'nkw&߇:}kce^n.8ꗑ7粆.֔>H2qr2N) %V8ImSpGLw+(w)RBznaqp :x ,zSN솭9ۉ + P[h'[$cCn-`0A8rAm*nRӦDN\\}EkgXٖ_>@1^US[۬{7v~{Re)- oZkLӢcqrjpRKxO$@Mu|1gi#{Xp#^Rw&fsSAm1%S_9xTUfw!v GLy*W=x| K>nKT62]HoNpG:~3rIsID$r<7 xfirQy^V*ygvVR'+I2Gi#̈ONI=^^?Ba);y&[7<ǎߘ5H1n&Z ُ^)oȯmj ^%_ȐGo=9i?6,\ vT%:ֆ߸8p RZ]NjIl3Í玦2} W)iwMkw̺ezºa&2q'r1›?7<vSiMDܕsoS__B;6$a6b؋lZǷq#w_A{_>%MَOAVҳM?A-,c\z*vaOfہWa89VN1AZH(q~3wA"AS{P4T[;pAOFLOZgǯNNj@ٗܜ>'%#ԱĬ\= 94TR JlX7zKp>l@fOi/3f: W*N -y@iF.w}J)3=30E'IoZ TP0KM݌,ϱɠڲj! Y#RQȖ*H< Aasx#FJ z5<ԷODŵ=?ƖKl>եz4`}޿N5rxf ;{ޓeH=ڴR"I nsx7N抚Z,У4]G##@g2Tz(51 x*izs'p&a19ɤof)ؿ1hlpy=$%cҜaRA35m g37;=b2|{ B@2xǧ"K=,ǵl3>c1r 1CŒxSk6=^k*FqL6Ŕ`Z!G_jbDZ=ǦJF-Y[Z-{qL<ߡO-J!)n*:"ҳ3>;wIm)|/1r8G'ZksIA=T6цc5C69r+J8' 4-i_DK(g5\ڿ^Ҙm8d֔~fS>PدfggAI^Zƽ[;Kc?˴'_א+[A+ = or%'2R$#i$ڬj?~ k-ȤvFoV}sV ZM+u).&A5d @RW59o:;[$<,R8Cq{W3wG㨴MwXh%Sg-i@o1<آOrt$?4OhE֭M4OjੌqH!{ A7G,!`1Rc|:c{=;QPړGngD0mNA< ֩]k9cif A psVMZ/󹼩+[VQ]iTGYP \'fyI[-ui6@1ӊ/m(taTaiL|( nx? 4Jѯ|3][n̛Hi2rAkI*^G2d[5]izF5ċ6o0s&+2x s\Z.tFMۊ/L<7,xrH<E魧J%EnxC庌[Ǫ7oHjz|P)Mw` -c'#iFJO#/Sg|%౧ZN HDL|G;A95o _,KY i@C@޳|D2Z>n!A >\>F3קJ|M_X#iB"3*LaYrU9jME&mFw5.=ՅѺt`1$86>K]1ZXcV,μ` T|N 64嵺9iD <03U<#R^W>%Ou,>UalҜe&חdlk>>&H<]!si%Umr|\J`qXt-:[vJaCiw|9F XվŎ7[뇉V(F\Q'Pa^kkm˦K$ȊI#ಂpH'QT&a%=.?km5n 1\G*N}=+ῇ)n~3wmDx6XDr '5 ,<)30Fsby14sE-ݬzEֽ K˿  wA$ 6[F[=J^eUM]0G8ˣ^K2X1?g~T90zf_u)tUI2#6p ƧFWJR~뎞Y2v|[Of`6ݲj}#Gn("ccF\D/>O9⫭ZvwOk8ubcZ_Mug%学nJ FJ#0qZT}5ʴ\~^*yMNXekԏ' U9Pedm|qdt9<^S6Լ]=q)dz ytѭ8Rp wY4=;9An0OȻ\}*9ԝAY-3'mD|1$1PwuܴSbnZV!I,rO4'nGJlwBOgo ɮe,\߯>XUĎ'j@zg?wj*JHѹ;a8nc"F.ٓ6{,zG7t@,'T6wAʖ8 q~^4I3,yq7e6x=oy c>gknNie*v[kC?o|#BsWؿA©g4Q :zUA&I )mFd$dS17ss #TJMw/=.ۙX?6[0I&$dl_m03MTD+;*2W ¡yj eo J/5nCd)o /=j`ň<`~4$cړ)ϻ-m8ns)(8$T\ǖ q׭7xdR4RNҸſc-9zP@2KNOZyRm82i>rHOzX2uq ߷4sƩI91HK+傩%wff+2Uss"LeO>M\8G&9#s/jөxݐQ0]橙2NObHQ (;۵dJ]{+dxJ{JZHn qhUWͭMx `gh7=zϿLcS%{GDidVZDM HʩQ8ڬ|Gm{T\471stڼ ki ܫ|/-bPη1,ie0ĄoWb{O伪[|)5ޡm r0y皮>h_ۧcF؁LI] s/^VlÖnRq\w':hQ42ă@߽jDۤ6wvMo/:M|9&!8]ѲW Ã*`yxJJodTGs$@cP0ps9ڵ[-LXۭԷαLcpeP͂ G֭x[_]zإf|䴖^4lۗ,C0jo~&Aٮx;(c9C9h|SԌwx3ֵ̱kdSR>c+1Q;:柴P[.tgjP|H-gֵhK[n HpIlxᗌcZVy칖B#]}`{mskoxakDO5VmKMhlVUx0o, ,[X wQ烵? j؞[f24JsRv!I ?5?K3[Q/9nSQFJZ:-Va- L>Oq̭*Oo6F$8zW +YUXݠ=,d>xajR2S?9IៅzLjc>f6$P؂2c99ڜzjО خ`H")PTʜҪ^eWu},C'u5i7-W]A4{aw9vwoxMk}0_Dv6}܃C+TmYiVxrDeiZQcpvNɺLm9+>XK$<J8y<2CqIԒۿ= IPN gԬ׌g _[^MGF"9FBg8#K*+Ҡ~,3^I* A3Wn~:xJ?>3=9@"lܠ)=xJ%?ml庆im`dY2ca'56OWDҵ4O<޻|{_u-*Glx "Jqg=1_*Nb2~˾9|j|Ѳ7i H*9`#2ķ@z#q{*ۡ$޷9|ҴR?cqu~ᖭ,@P+Gًnc OB%I;GRE1böjH1Q} IZGa_Cg+L;V8iJ\;`dz9-ϙ;'a#bFry"K-BQN[:s*ZerKT48,C&^8}jOӻb3RfRI[b%?8?OZPxx3RFGOqJ, zeԵm;PI R&)e$ש\c*uN. Sqc#;`8b =Vixϯ5`Y|SA=T' RTܪ<})A#S6!K`r1ێ\qX_*v2N(j뎦5W9Rπ0sRtikB%VǯhRXGZ$z, #}e&*)܁y2iZu5ȸpVdO=)~$;rtIhKG1I&K.39=喨B 0O.A8ҭ 6RvŨS6qjTvm z /$ .([b$$qL7дPNaskt Ī&.XzͿ\eS[Q(#1_fi4BNw##qS+\Լk_L5#LnWn(wrG:~ټ]0K(0r8L)WOxW8MIP 89%ᬤBQm[jj_|Q:wlOp&bVVf=ӥqk'ħN;[`ѫf|?*$c]Ï^ \ 4Z@&v`FWWjE,W>OJ7eq? |q>L[tZ3 .=ҧu)!ǍVZKc~S\Oi^?fxJzǂ=[}jO/P/c2YrwTt>5&{qWD {X\]H-5.%F}Uih^j[/ثm)AO~#E<TICGb@A* KamHk X$Nq]OuktM>o3J;!Fdg%Zg>%yy]fRij;H2>6_p7RpjsSy3O֋MLQB)l=olhW#-}W2J+8ݞF St_3×vjtj"3=i(ԗ f7< 8xdg[t{N,&X̐+݌+XiknfbC rryG0jpiuͰ,*B*# Q;.iV/kWAH]S 0pBHsmg 1xcPS᩵Y|Ai6]%|+C /#>/5&+ e=~Eڹ$ysqkS.> kG&kmej$ʧ*~=h؞ (㨦y={+rIs=;Ғ#?58^.@3NZKlasnϯ 1M+ЗI"nՇz14ܝbp=K cتJV]ERǟhuq6x I dtdpq+0hs>ТC2y H\ U\Q˖rTO_zwn|R5y,€PF x͔W'sض hYnLqxs^~HFLTTIIU eǵZrq=M" G?*kTN{H#|{f 犝/yB;n5RI^%`\sR1Ί ɌcLoYiKh=Ҝu\ީ5–==ib}9Qv^➱|Įj reilnEB8`VNVPہ^R|W-jǐiTqgj_~ld{(sV4+M""]Ȥcj=7/N1UDqPۜp*yli,c<{ |'8 Wk1ҴRx\nzI*1 <y^ ^5*j+cT-2r `A~X^9Qɨo@={U5S\Cr9힔Bzw5OCkm^rx9p## ա:˱Ʊ7{sCpzvJ/ߓӎi o=iȴEO9)dps{H`nB*ٖ{;s0A?TQ揵'UMj Ke܁6Cqr9]6I4վܤmV\*Zt)Z-H <-498':RededQ,DZ㞾n' P\{2%@*6@_UUDܐ} $Łl?҅R䴹fHn^3;l㞇{2=G=iqUmmurU Q*rAQ ̕9Q 4ƺ +m>*3Mydy dVPd8+' ӑieN:fkvUc(Vn:c=Px~P^a#ptyzJ FN ÌTaJ#'895x# z(ܴb`7CҚ""Ox8hzU5nME]icڣ{ҐW1tF i_mlv~CxgoKEm+߉irn*4J2J91־g-> ]O,ݻv:WWP8pmۏQwǺfysXF^MK˓r]oLX-]kP@,9p'np1 qR Eiߡ%?XS{)F[iVcULg]ॻǐcϦkdpX*9YDw!^7-| )cYzmğq (_K!,ᙎ>ץ:iݿj<-jZ?}FHIo6@ʪ7;Pw~<=y7ۙasυ/ ՏP-ߞdl ݝyg#2uX[$vkSDDZק+3"FA21\xmϥ6ҼMisy]xp;i^Kvk&=zӫ~nE/qBqWyj7*6ȡn%X$Y|9d.JtR~Z;-EiHf%Ks.Ic]x)l_?xN׬8v~c֦m[a{>{jq[rMllA,zF:]Kp-R12-'5xֵ^]LTEe)델8[lm5:.$2*xcլ^&F"P#|Rx^귋"!dHXۚ͹a/Y&g7d3f\=OqM&T&RG$1*7.Ծ 2sV_'|kumvv y}lʲ19ϭ4߄,xS[w*SZFǩZs{u rдCyReUw?){]I~'k̑}]lr\ۆ9\߇kjvz77Rh[''zuOjsYgM.դ+h̍cxr:!NQ[W_|6[þ-$vmvU# |ic,,NKpb9_p[rBH$UvrSko"g7\ik>.W #&.pOk9Et#] %~B[>f >:Օ rLc%\.lr)p>qsNOb0x W0}dql N؅pxn?uOBzS?Ƀ546:Q]XkHv!ϵ8;z8$M5yUI=AcFX<~<0ꀏZauStQn0A 4 zZP;xޡx{Rj=9Fq,#{O&Ul 1/ iZ/9 >K]U-h.u@)=j$,j82n ͑8IVhelH]T;Ι<#4xqHdoI=e.J 绨@;?1ڨG^s:yຂz V EUp9V}=P"=2H;ti\zOJ,axnёRA80ԕMP0O5FĂ[R. r'5]hlz!ρ橤F)+.J58̝+6-Ņ斩'm8tdl";F;Uo&-jE(g9aU/oEFZs/g5g\ , uAU׬aV c?x6W"UL[Eoa9.s> +Em,ko#o~#z5cw?`,ks ?omT[H 'ps]+}.Mc\Rqg|0p}Msj!%;\<00M]:4vdRO݉K虬ѕx##Ҡ}Vv?z}؇`xvOvIaqwrNI$V$ 7_yl5Qs?/5N%`hN2C .=0kE~|.z⚬YP>}g&6n17BpA>]O ŋ$ EFI|8k&ŵlYH?\*Ncq8'Ck}O.Mښ&in -)gp9F3њBa@,FXBAcX8)P|ǽ$ȕ)^BMrN[>I;Njk|.9K?#W-5c@ N1ޚ0qNiBV)ZPtnL?^vU4'qS]X \Fbt.~`>Z;3H294$(ldhK!75H(zs7#'w`&_0s)zz_S9ucąF@$ƙ,PT`nZic;Z>v.*KAXvQQ$:4'n[$!kd @"1rsڐ)R\"]ǀ0 i7 8(a;Z %}ƴHY$B +ez?Ni #zb|ߝjR-1LbFz w7$ރ$dym45gfMA) N{S0UX)5u`> xf4ȵ;p|лƒmI\q|;'şβþf,mVU׮},o~[GR|KkpT!rXGL_^Z\"Jgؒ!䏛#'>T .E;Śtigos D+.YrIВ*+X."LKM-Ā}z֞ڮlj"h.&xgėQqhnP2lJ٨W-T' —(6souodD'fO0r@By< [kq VA_aoHm pg#ޭ[x፶n5tc3[N* \<.4!G=+H8?Q6FW/xNܷK.mkgeey=Z?x3@.t mFQzFM [8‘Z麍>i/.PvnU|'rώi5߃t FCͶ$1bqV|2nT?n|n֖^1j) e+Į0Fxŗ~!ea9mmŤ|RN:W ?t˖I.";q&U]?t OL]V[TDA0a3UR0V¤8/EHH DC$^br۷*T\2{(cXgc\@ [|A7/̺Fn [A1S8xxU5 񧋤8 oduqS $܅>i_ز~ :%)P^5, VESiϗ p+' flu_[}+veGn[Z?ielLIulBJU 5 [hAcgq h)_ݾwrrMC޿~U; BEłEuX[[ʏ#?8aHԜW%cH[|Yc1݋yB@I OU_DNmPBr67dc]~"?'.luIފ#sXr%V:7կ[kgU;LԤ4l͵ F3Ŀ iuM:)20<G?\Wz9n52P0C$uOmWK_ǗZoȏ4 ,_^< NѽClDZDp4v[Q;WVE8ӯ>&i5]j(aa*ۓ `snH殗X =Ŧ5M-X#@d ;wO M^{N2̿iL6<XQ~KjWESៃozI|Q24l,ַR ,sa{_gapF:~K;Ul93߻܉% T4~ųvH15P,(ǧ$sH`e}Bƫm.9KZ6}M(SI->5a࿇Zj@pZfY|3 u_ +~J3 ]_[yG;xpϸĹ_ŷHcc4%@p*|/,o oƺ m'~tC:MBC>pW承ZR[ݎ5zz׍5[M+IѢX!Ƴ(pN @8+m|&+{L@!k zRuepzlbrc}{='RjNV*eg)'+=n/YM^Ѵw9Y] đG?.Oj3k[Y K]J(־uF^c*ɻ'⾅9m+ۻڪN1'CjW6Msg?SEGV6c߷[d6G\:SCZx?j RZvG=xc:P1gI9Z|ct(TZJпfsOleVda[Tԛ$rҟҿmx *7Wޒ]-?QKݹ(}_kc8YN8mU@}Y =*T#FN B<#V@$=r;՘+d凭JPnMB=MU5بlʱ˷9/˒;Un'iPd8;G _i~À7##޼䞽*B ]c1I+i2@`:җojrW)`{MuB:t>+boa֐ڔQd{UB-/y"䌴ּUc>xg2ye *]gM_ռ[+Y/cXmSp*; /\MCI.#0X4QMe鹕,zCºZ8e"nD%NCGǃǥL\Rq9C-z7ĭv-^m:onc@Ι%/.gGjVS帎D&6F«8fө C֒Hq1  rnNʭ&U:`xAB]ն!}Z&C>Gƅÿi>-{˘m嶟,#qV} ̰[O]Erưu \ |5YJ8+[PV>Oٚ=Z;;BV) *,,}^hɨiŬn]W9+zZ|+I4O_㷚X`0ImD3`yxDdIGˈ"n\IWN Fi&!|nO|U8E i<[jVӫ[E(;h6X?+LSO'֚5x[DE80 =k3^þ#KNxeEΊX I6~xLRדϧC(so1z0ARxJxJνZݛ0G3g,dc۽4gfmXw_[:/Mֻz$mVK2!yVM|qghZD]91$ 9Z^kD]"zIsue 1#).%v^)>*58m-%wn\"Wel&UT c8 s+OEc8K\L])5iǟ2䃟P9/W$v$ʈpJrG]ZYXˤh[!Px*.8&ӵ#a[HDm3OM)7M%[!i6^ 3[ ob%w==+_[Q E$do;R;~MU˦C 񷼎Y_ݗ80w3cڦ6'Av4YU%U_OO5*\JװJn: әY곛hE_3#bp!:} 7àb*`l͓ɮT־TqZEf![*@ NVO{۟|EIAn/m-Rq倬0wo#)ΚiMNG^k+~ 'v`?#.z|O=/ou !5Կg\wyG~ i/Yׯϵb7 p݀5~UFW4rw+ lKo8Âkf>T1וZ5Kx[h!pV}خMM"O5R'M5=b]^ݲc<J_iErxq!I|a|T}OnB~@R?yH@\~wL &RǶhH|}̊7A43%^Zt$$Cf\<u35*TKCҮ~%#N;?2+*dbMZ0s\u0rV >6,bMfNAծ%HBz,){hpWSy(u!UnX'VT&hs$U ??ᇃϋ~$2őCjQV5^*c ν#n c?Z~\m,`_RqDp?5_ 4žGcRs$~]^Y=ÁG1ueX i~L 5o VķK˅v-qaa7?z7 I3]N2OҲzLRZ?,&W2Mow4Y,<˙ #i Dž_S9^NyRYH F_+88nQ)vsH*^|ZBc gS.|${I.95+zR(\MD cJ>f $ ʆ!>ee xˎGJ2G-2)svi%KyNs,NIULr:~ԑ98h3Z0oT]Kv 4_bImgƪoާ$q3h.im}MWE|;y'J>TeHoLjLۙ#HHc+4sXԺeۓ{;~[>8=2[FWp֫wjd/N4'ڴo(,ik~c;zVff٤8քJ,}u;K{Sߵ0HJhvwNPkۗ샵~϶~qU\rNiU0GZ/zZF4iOuUWxqc1 em[FIP.m}2N> ō!A}>6]cwm;[^J|s|/%Xt.; EMn!1q>V'xLbŖѵձ117pA`zWou)?v[*rdL.0'Զ0~eg^'u[R#F,J졙Py"?ũi6x!4RG4}C]BiaTtrx=pk?':dZ&S_uB: &+lXIMi~;olZ+0hf`w>!OS-'474ۍN(-涖4VO1g;{]şt{vC K6'uB]R?tvž k)",e *tqzt(Mz׃ZI䵔a)Wh>V].'$vlnW%wt9)^Ҵ(4Y|K6YDmFaAa 5N]>niፀxNTDԣg^NjP$/dF,H@%at;ǵVijx^I-eis|%쪀F8P_cuϢ9|ƛp+|3]6˩ %BA&1Źqg,ݯd\=zFx#q[΋ Xywx >MOqr]E$l^H;J-|/k'J% \: ͝uxO~􆷛Ac}.f6"tU.v) gֵ}ou0(F_t]O4[CPk/'P))ɒ76dgVS^iq"\%d?xC TqigXxKqV^,O3tl(x^}{^&ދᨬ+>vl C2_xحjqaKa}[$`TVOSdK7෇P>b3Ky\$ 3t oݿTRqu<!I6|`G s.^xu\_ZC$%˺7?$88$Sͽ/ o\:T$2um)+Osj-ˈ,^y"Cm܇T|_.ŴִKs LE70U?y$5<⋹|5XX` `01j^qީ;E~&[JJvBV{m~ iohk=ܭ(cxkxOFxo-g{0@4zeqc[\]jk " #Ed ȭ܀yWo鵽n}VM r5 ;'x<ݼ4j>{'_<=V`j1(H-q¼kMSk3I)I#gB+dtLANbRarr+6 .Oo_W^Gƌf34 C^_.:'fx=ATS~ryr}i?s? Ҟll NJ?^?)x+HL4N8?:\~9&~#Ӵ`neA˖5+ nOoj+iQjzl7ğ<_s4kz`VٍkOIE/p3tXDL6d~'4]D`G^%2n9d/s,ǿ_6iu, GVK6 3wu7&i-ӎG8Gx2FÅdmz_Z#SVw{G?~8r=F2!k)t|#[UGX&X3צS ;?<72Zgnr@ nirZԛwn’4ѭ0NFN?Jܱ,4?,s@\6X0= *JN܆mprid.b}Lf 0:X@Q&/+7@_'H3ڜ#銍d[w~`%Mfޤn;HAa{ cA$j 5f-ʣrUrQ҆ooΙ#?ZI݂2H攖Քܧfp 2hӲت ׭=`2qYA8>jg T.kAil\6b193GTNqN1hsYY|w {OoLUm<͠{g؈` 1zI_D[ܮvw=5l[Q0;OTinKmJnRnߞxdv4j+jʊw#h2r qge$ V硬Z>n^`L=0H?Z9'6| ݬ\)ls Y6sVnc!W_xQU*ri-b@&%:O1$=*ɳ>HnqR9+lZRU! -z !@;Uo0I4b݅/dPrE0)qޑ.ҭkH1`rrOVU>0Ut֭9r@T5|K^ ̹w i!GQ`[G45.^/c}I-ue90 sh V+'H®qzUvI2YC gD\c֭ު2Hmst\[6q 6O?ƣ s늶-˶Fpx֛\=z^8?] D?6:bw'$M\22|Za \fVG+a=GɆ]Z9.--]Epȯ:o#N5xkh6$fcQ1}8}'S~ICs/"Va5tE r $]is5*n($Wl],N;x<g131qHiZC5p\n.P "P!]dq|Im*I5ش6$UX<azo> m_'PpOAWTYiR0m!Ԯ`P{vP>P w>ڭ':[KX ]2'U5K_43^jױê*t2hԳW'!}k^n{$4i{(m'䵹mPe" H$N1j-@|7tv[]m7@H.9uosqrUGpjc+מz՝~-t|%s42Ii>ג0BF22pk go[)ᶿRĚ.J)!\˟2&Usr}kDoh =y- ndm\g$~~")1LH-Ekh5+okKq0X#2`9]fL<^1M_Ÿh엷3Obhvbˀizwīry4 PE&Sg#CBryRMK*io}-u1{[K9[ )Hm8_5=:״6Q{4F^0gޕ3 =#QQq` ͐Hg$8v? in 񆡬[ϧ%ZlDB c?)8,.7O웺/&>@O#q,rEɕ䌡;1i^3[x2_I:Ȩ]oGͥf"kn"x ƙ40cWv6s~_&x2Itx/IuvVEW"{6z v9e'~8+J1Vn|/j9i52ᯇ?a'xD8Krߦk|% []OzEr[@`Tdː}k7jҎסJkcBnU|a@1V}^x&ăsU1=$O_zPy T>O/K{¿Gσh*zZ#3קS.sm\qZ"1*"&XsڢUk7^Lµ]=;m" U2Ĺ9};MK&ovT_h zo![q;9$atI0xs$Kp ie>\:cI+F?18o'sdd ktZk8݂Z/a%sMraH'<(v|̀O">^8Ww< ߵ*g9oJi| FV{k{}dHdדgzR M4O1$ jpzGM?#;u -/,HƏr<~2Bv|?N*7b ğOaҙ;6̶rA!O*&Rqޥw889GFjW`s*J}=jy:sjE;N3Q{nTW3N`x<+€ _Uf\;髸9J-.<*sOmQ-HZ+o `F{~Nkr63k9ǟe-uQv0WG S=v–?ݧ)3`m#JbK^C>E/`c+/ HG%=&LI# eccӧ_O'c1BQkB:ݳZ\s3c=*8bwՌeEMQB*D.5>2 d9I8H/ >8!zRF7sT4x#ޔڻ@k-nɐ{ӍNOJNFzUq#a~~wʳ'$(hkq%?=D@qNrHVa.>sǵ5ӞW049<՝%  ho,/Z=ݛ)8sK9;Yj,^h:0z2*IUUfe ؎1JFHVHԋNg ;R. qOj x,wz5S4 H<.vP_(r?g:`šo0r-jV0ꦯ93H` ǸΒA .3t47H9'.1jژ#[9=~~2~K$ރ eǭfs7瞧? z`*JH!رo¢e Ǒׄx(kWx;Κ/\ `F['NI|LpNGKګTh!SA8C0=0{qڳM=Sޣ[K1m܎xTٗx&}QHF99>InA}ivJq zthulɕH*W/~b,sȬNH(qCS:D:V,za坻AӢǞ٦db D!sQ`ʼn`z5.)Js^1 БD'QI_`6In@M>k{+>[I.ߠ4rpGL:Tdf"qulN<7L(>R3IG57cAMD9U'$=Z`x$㧥Ct9HߺسGnq < `sێseWMG€ yԍ_yoT6oyJCɿBwoW8oFK5+' Ud3]zM6zK>?~pjWzS`L{T䊧-/+xVߺT. a++$x{bmIefK,3iKm]3BAv/̼2T9RVwo:~(xFeNi B OTϊ=G\jVw2Be0 cV|sFU_%Xnb;D0\oWZ]it)eGGAz3^c*k9L!xA=*xWrbf$j/+ cOK]ȊM+@ 9krX|kXIv>wa}b\#mwZgmsbu#|9霌u>hBo2ȹKm,H,@'Zw [](V#sGv,9#(pFzhҍHӾ} K4& rW/RG,|,.OG5)M &IljbQT9R^/}薃 |I`~F0\[~G;˽ҾsD`tMh?fV lZ°&D@"I;Pbl7$C[-\/\h;$OHofpGNN*]&.B$im;j5 /_AA9%}1CqMF/rR8 $RuqPy<w[JjKj22HSI g5\J_0g#̊p_`yXL> Lr)Bʒ3qҗɌTk a!٦O@AF02Nh{A=HH$+O^4FV_$y9sNMF~Vۊ$leJvv=*7bsqC@T"Pd•Qi^Xufg\cƢw84ʌu)[@ރ̛TP峐N)f@G|.TcgJ}F۰!Nz'j=qڤګNw6^/DxC]_F㋔.ܖvfI83Ln }Ioa{9yjxd9#=UrcqkBs8 R劶;*) 1jhhbbtr@8唲;{&ho$Ӽx|jS1 '$zD[#;@UR7╷!lVV&#C;f(eV8a1i1;\~|X,Js 3Q# e$KmlZ2p:Gs!~SQ* de`N;}i]7dme-ˋ'ymzU(ǖȎbf9\ǥ2I90WRwt )t4WJio|TRĨn8PM$4I*}L 3cP㓜' ŧ<7;AyߐAU,AwG_Jj\ ^d1|OsLV8ێD_XhrA5gbںГy FwsR$i8,rGzSY19tғKKo%2dN&bIxg%;OM v4RC8HvqԊ7^i:(>ؖrI$rx9J҇IcI IQBx#(bUr)PURylQm+YOJ$}=,efR|B-em&Iducf*J ;|f;qoHa&D[rLz>(OY5ڤhfۈفX'o+ȇ>]ZXPsWp'uSJwKE؊|ៃ:7ߋs䘝"'b0@/~/JOu$ c.M~F LI6bfCy|m򋥭]1BbR\S嚾U`'4(2,] o_.4bc9?Z>kek=Ke j$1iq~\4;) 5|׳Lɤ2'$<YGȮs m%gNTʲ()8Az? l<3n?<0CmO]֓7Eo,q\H&;r)\.Xw&mE-)~ 46sqi# =$n 4=F*QRИ+;!?ɩ"U#q{SNaJs# fr9zщ-E "16*1I1]L*`y2sߚV,qjӁ<:!' 9Wb\mm*)9h} rI" PG92 uH<TŻݞXa=(ysFT cI&9)m\h6wXsQ7x' CžIԑ2+4JЄ>89=q7~pb7~@Jf+ J;RVFmWlz{Sv}0}Mgـ:k[Ë1#JGszs[8J ДsLr g3{uQqJvF>֑,v IP 3Uv PQO G\ pleX]/#$ug}I&+g׽f lbF Aڅe)ϙrs3AZPrr2I>$╇/$=E4OfX9\u<;PJ`p(Zӻ[?j, )81Ú`9v7w{ |'Iϭ y#?.z 21-L2P8uȧ5rhʊ}5:1(=}A2j!A=YC {U <}?uRyNj\v(ӕv=;9㊫s)d.?3^W|uQj:zJB`UF` j}ѧ(OFY0rF"NX,NFiy?qySЁ֢}z/hM.vWSIi#g͐+.^#5V돘_Yk1chN8:TnH,0i6ﰷړ7y.p?֐YZ-fmA~=rM 앸 /%G@|/\?x Vsi$gL,:v_3,/~²a(=GzimpcF$h?Kh$ b@R}zr욑kKYWHWj %@U U'!&mU!01޽إ}>Si'R8<]Ej[׭ jI:WOm/2P']D5{-JZCF1zԊmçP*!$"Ʉ:-E S,Z X/b=~OؽҢdwy)%ao0'~H|TL?o z5Z+01lUgJ) Z2z#gB ,4)Y7/ˀCڕ_G#m@Gfm;z| n΂FFp{q-% iJ~hrƩF2ck4Šch:Sv3S[귷v2'&xYUIL3 1]vXi-'`[VP3fOCŗ(xk\eK@ܞOjc⫻h ׭$+d2)qnl@9ޮxJ w)!SXONṷ(>"^+DB-̡݂_?Y+u-ߋu^YU`G ryV\瓌? lyMbe=e"%Mk^u_ژ\H?xrk^7K!>LO J"s;V𼀅9aҝUtF_Ao~+xH^vIG=+.s^mbhԴpIɫ~XJLZ~Ծ ^ܥ3u~rx G$n,AS*j >H9m7!3^1m3W??j]zFC;?Ozפf H]z ݻלѦ{m:d6/%OQU;vjͤ۴q)UX`w7^헨$U²ZeʐpqVu8Ht4sO>|/%0A c9_Zռ'NA\ki'ƛ=jsSKM5[Ǩս3;kT-%j8\~LYM@˰J#NմN6 gݐ##=>N-=>$d/UGd)%9_Ǐ [oNaa;o)˯$7}䑤][_Fi%ztjQcxZz#_Yo![Q٥,19 &<}_חGRmiy-ŏ2 u>u.KB3:td~5+ ߑT xFs ?R1 8ƾTn5ӋD"9UIUtӁ@gGҀsiX5]1\S}\9 Ymr'SuG$ڪ`rAՓA{`֡QhW\4D zƆHFɣef+|;l|`eYV&WJOޒ> nqKe ']$?^' 8]F0r1tRT쯠G se;=4و1QBf ƪ!5 z˜9:ӰuZ2=.܂>Җ"̗Kf},mJ6@ʑ-7J$!AB Ě#o"xUrH2یqMb/afSd f nOS=z` GؒPZ^{&=~(o G5ih I(v 9~ƚm.ˏ&IO' x: 5E0 $¹.M-uDXqGf si]9+Oھ2)MOV"PV8ک=1Ӟe`4ҳ*1 #=:qR$tQ&$n}MD!Mwp3 MH7*ssɁ`gӭ&`л2 ׭=a@>6î)99JF#F QOEqu``n'q}>b@B7$u4VY Gm@<avW)p=X W؇3'!Av8-ǽ!a}y46E):ֆ&@ieAr҅SPA߀Oqښ(A9}V+\=: Kpdw dfU5 rL_Yzy*3R2N N. ~58bK+IGas-1-m?\~^|=6ӍGnIoҜc{$MҵawE4]ϧ]j"H{]M $NQƫbK?vL1ivaC zU5k-(mﭭTue kI|_jZc[JW?48gJ,y4SHPrWjƩ+mO.)⫻AԙsӴvēV}ω x@1 ֹsYZݝƲdKx䛟zhIًrY-jz՜mibKķpyz  [( 1ɪj:양^89*q€r1Iicz3mcեb&YMGRu]B]!_@jxvsyxnX&{3= '#kp>Ƴ|LIYHT?ږ3hz reKp2K*Af?\j\";TsM8+afώJSbr:ZۙINK3 eO3}y[nPsQ~C"nGKb@Vi^NsV齷;8Tmϑ(?δ4m zqxf }8VwOםx4^(tMA;-x8>$$C^OYY-9wWp71:a'$M_:- RL&$O^XXgG%.#v`>Tk}k f2G1)Ͳnihk5FRxtǝQ ^Ph.仸r U~RGϥdz/mK#O%ǗԟjdjW]$rwnWh#5m+oz|Q;A'+iI?ZSkѭ,~e*xuIfavUnA]j֢D`u*?^I`/Lr{]|%$mZ)DAZ>HJOj7b2 Z6 Rm!B`95 &qE[_[iu V{ƸzdUbfe`e 潳Ima+,Z Uki7 %8sb(՝崣cP&Mow^VC*Z S?h}>)gqH>Ǘ$wK/($(y`W&ܚn~Sw~m#7k|[~]}tlJ< Nm珗l?8sZR}~xA*Yt=ԧvO#tTӡB?U'%kk?s"Ja#U\;G%C;C :~vC>~RoI:ANj7R\nG冭uhMZY~!פxC4G{% ">߃AZ-E3wcdG^[x?:߃Sje{+yn^IbhR;Fѭ? jgdaY`$繩J/'V:, 8g/&ghS#(f'x8ŭw68H8>J?)< JQ̿v7ZT?  k{ #lP8`qkUٻ9)I/S<]XﭭM4jZ%3-glL)=&~]O^hBA oMӁZwс3i3+!\ZqԔsMODBa=ıȹBjwhP[hZa.Ъ@,+  #G76O(e "(Kī n hCor2$1ƵX,Dw9%:VT}TO(qS].Fԯ2kࡿ ؑfˏsޭ~|&sHpH8 +YJH;4u_ٿyH&pFӞiwZ>k` @n9zu|s0y߭%M5u%-U͟oT߯/٠Åɍ5Vgfb,Y\ơqzQ& T.e%޵oA m8q|M. b=gG0q؏zlt sjG"&u/ ubq<;{D'˖ȟi5G(-չ W5$$Fdm}:͢9HŘ3 Wjym 0ԫU|ܜSitVTN@FiV3p[{2٬klxji?ŏN櫏 3q<*y.;#_MA'A+\I͵}LdSeQ9bZSLIU*Oׯ ֮Si[>N׭wn?*'HlJ l^VD#- 殟^Dz\=i X.΢4tf7|=kp8$]ͣoPA7t=sZD[zR^( % {OJ.Wx%̗17 =gSS@@T29+YM;|咣QFhF_-9FEKonm 溙GJX48@SR08V#m [ٕ#ƣ_$qjc 9S2|( &+\_=|4XzsƶNG8z{`ICl (㞽i-ܵ9vЮF]ƒMJc=?ka5NKpO֒MVQTc3uVSUUk-Lw/g A 2iWň[I y+xV[#($|rH1HuF\g?ʢP{rdtلtRL:^0$g Lia\y<{to!zMFX 7֪Ri%c9;ũ`lq (Ow?^:VǛbK--͋k+#.29163wQ?˴z9V{?ݒp:)MtIG_ok)A/Wֆ\O| l ץr~V2K[2J‘>(,--ԈF[9#s|DLPmܝֽI]\ᛍgFOT0אCҩXsShk1]۠t@zmb]qLAѺ)cTPg?J{IGJ>vIwϡ#5v~R~GP=2)K1Zst4O;Jǁȡep}Fqi)Ufr9w<d*W0"yv%Ia>zlxibďJ@zQmn)>k"H Æ֑fZ~zSnj=ZryyC@zҖ 2*vr'xPyIk$@$ڕ' 5qQjw[HsonȫVXcG@iwf<>\1Yuh%yeZVɺ>z1bO\֊2y^rsҼf>h6(g?@?d/ķv/ӔTuזǭ4N޳m#>qo*d'U5)Ԍmk.*6ҳa3٬ܠ4ԏY熬MZ)d^)F+*nvybǎJhlŃ3U}bH-DBiZ%ޗyU|Myh_6c6g꥿Q?\i:h72 ǮO4Y)E$w&TUݮQ>i)f3؉Kd(RAҾ(c(#>a=pPh?YN4`sV Hҿ>u-a9"jơe|d+%=Y_tRpS֗}(#h텲9iJH[qO ~__4>:l,ĜEA-񙏖oE[RGx^g! \ y5$?I G~r%uOp? EJ8#zQmm6{SJWԛJLmc;E#Sd E*N5[iTSzVWaЊPhorKsUA}:P(9jRԞ9sAXc<[ H/|v$ܜԯx.eFޜu2q뚦l=i*䎕I'+6_}]W8N]x@y}9hCGdsMvM՘wNhC sمM64\(g<[!~+c JT'SZjI9h8RV=ݲqҨ.ۘp;SZ⼁ 7cn?*Txc`tQ>5$|*B66Jr֢I|<cLqF)D e'ѱ=Eges=9^RRS]5@ 9rӯ]]&W֣%ps uHT;{D!#%J|BףP-MHxWTn$k#'*Ojs:\Ujw#;yvx(=$s^9q~џ-|y+?zמ wgpiBn)M%cmj6f"* *0?\5v+|k2-:O@gwZF4ۿ*JXZG'J/OF*շ_ty/|?8 lϨ?ZIݒN1H%BZ_VwTv>|FxjEpo)׎o<1"0e#M|+8c9+rV#_u_(sd`٠^>:S)c*'f}qoTՒ@/=|BqI4r*L &T{1PJq$qXKJ%,}V+xQ4)<.-c'?|9mO.Izc_ke®} N9u%I7 _ɜu8=H>?qqΝ/?>YzR{dUO,Z̤~N@-Krlp$T̪[|&5 duAJ50G`=y.~~Gz%|Yl"QԮ3Rt+hCE^kMJHN{ I/D8VUOVeK$lu@;x"ViE$+FDVv'iڂ so~q,5 0D#>e{t;]54 xxKIlPqjFLIc+bhc@` vq#//(B>^J>Fb8 9*9>S8݄< )K2)ޤd48'NG^Ap9nE;^㌛#' ds"|˂>a;f *`s{gk4&sOR0NOGPgm0RjC nʏzr /M G'ۊi@O^=FugP@x] T19+'$>pYP2RM_Pl瞦f\j7J|ӌt m4fx Ztn^E@w8րٌ׮k>/xoNխee"h~=N]L q%ЙIIhvKٻCC8~`;ךi֮Tfi6vco~k0Mg:& "D>TgA)t=5w@@{rk,|l~ [I-o8cp˹(?8#*G#ɏʼn*~1UUU~.sN]0 QY:Y[;ߖ>m==\9,cnamUzqYMNhg|pAqӵ54H6leŬj=rOs^$ͫiڈAiNeTVUn|qT{ ®w-UNx"ڄRxLйI%,ns@NIT{=?3qQ:kL[0er3vgڪ>.|O[]ZLu0xvC<贿/(X鶖ş/lH20qՇ—2ǍBpYYFZ|U)^kaL9q9h>/P[GF1zQmR#FF*-OPnqv[&ꪲW[vL !&29i/.ae!?λ}*TȴhQV3FOscRn]6fG4-B D!ԣv_H6HjJ63;$1_؅^Cu=;OgLgoi-V(#F"=zFDdiJxR"{J[qU}00$͎2֪"Yx; 2R-# :+?6]%M$fFM ;BU>^uI^ۙJ]Y+/3H)AUMe\xIki?r co&K%:sHU$ 55G_ g<_bܾQl_檭$NO%z^;@4_plE]2~,+j3t+F)o53,x^._ uOx&֯mQ Hy}$pZ4KM^/LܼEM*FGȼT:Fo"p㤾a/Mkm-£<͆9NOYZOiOE%v`N=*dѮpZDVw>@sN="Ze+!nnݗs:^ǖeUl$5#_@I+AzgxUmCT Our(' O4a9$G~EX}q1dȬa}tETkLuo}?U2;%XLGd*,OZ4gI 4kWY\HۜSUž V^)0EjGsG~xm_&p~g$q_օ I8${Rdi/oc [|>Zzݙlq~)i<jY/#yਟ5kA\Wqc}c耽mP po@+3[񵞉ͭ][ZBJ#\}I-O`SxT3+ӧ3Ƽ^ե' BW9/uu$c]̜^'m>W_xGTm9=X2g_֛]qj!#Wlm$qLmYm Vd ZG>&k:0l,o 搑9k|QyS]f;B8Zw7w+ JpjsLV Rvg풓I)+X[#'&4i*&~vm𞇦kJ|5ŸIϹv q`U4$^1Bnϕ^$ o\J~'xԮ=%^tvO׊' T#듏ҷt.KGC(*| aծf5$ 1-M15d}j,yݨbF^Ќ`Wc$l>I?>q_^5u]ݪ)~Ȩd Ѱ+E3GOZ Z(Rw~ޣIXZO3$_Xu">^H?uw_\WShV2>岶9 ץUTqi~b꼝_8·mY?鯿ң?5O(4fԟO<+yЫY"29T LɡiO(N8v쿭:vgR|9ՁP8ikj_js—*+-T}85Y~xKel;޻傝5[_|`<[Ff_X}l}'? 0SϠEq:HC'ڳcXyO'Ƴx_SV ν9Ș6w@O8T1m!C'r!T@T$k`G?q'?1T LF:o_dЖlW]ޏ2hԵD#Dg?=k7B*Hp|a FB\q}eY݁m?Q.A*Hmɔ/C-g#i%Yu%bz Aƒ.)LVݽ{ɒCխ)cacSc"oP2T郓NxJM[2-O|-t<(<qzԗǏ>È?33țsI@7 (VP8˔r:d=y4;[9N= ef? GץsZt&[cvcs;f,VeB[iţȘ[9PCvÎsG*OCòH8j0~ʅDB"3‘eNHC _P @ H I"W\!ǿ GPiM%L[%h^s.X3x{Ļ~*%Љ &y<^nufkwG ->9zz}>J=y@ZwU1N?,@V.'wrp߀ p3oL52}i𦕤u T3^5U) P~ޑ89]|,')K2aOZ-Zc]1F8Vsֺ~ jHO+aL_R|7Dϸw*+*`bsJA[঴r4xhȜtL=޷eJڜʹ*(G%roNAn*65 豍:FZRJWa:RZ 68PNGi$d0?XNOOl5d6ơ>פi/#aT4ޝˌ=^&]JM7i֓Tдx5-f.6 '9:TeJcnP`fH?>BeLZRIDjc8mXt_`nʳ>"^OgBnQ>k>$j 󄍆Zaj#Kh.GCMԣ+^ƛM6+$DN>Y jX!'gH؂qq5 Cnn1q2y Ϡհ21<ߥd^џX&(nc0o)̪ ߃׉ fmtɼ'7Y[|--<>Xb}kRKOBHY>}R: du==EmCc^J%=t\ԟMcÓZ$ `U6Cd2 }q5BUckde2 z,ܭi6*@=)<7^~~mogl-} >&F&O'@f!SϡɫQve8Ĝ&^+Ů>x+ii ;¹g~ yȭH]I┥+\!d}9F5CRNleE?kMn}CZկQخ'`I*vjf %TqpgFAj/ew=Ma(>oP+Z:ъ,+^UâM2*ot$`)#t⯋/E9$APTbsY9E-5~5tmZ1;R?'iXw(֮mkfǕF'83_kGR3 7g;AWz|UZ$<0bk9#'8Ew ?.FpNc}60Di<\?.+%2KIX-^Cs:r]#f+PxυZ&-0M!V gzTZM#!3ش;D" n`r皳WA\tRM[EL GF~}/wp̀=zWQ.>n`9NBd_WЯ-En[1JȒi€xz7.!,I$97"d&et 2{KuҮc}@5N´ubյ74m=NJ \+HtRNsܤnC8KņoO5;-dxAc#9Ԛ|S>_44 $8DZlʹٝ3O zr?%,,54יqۘH1-=ݮtUtmWAL|?(o-JXVo%?%]yw>$d?uS v^)Urryn#f2+qTP!Nd?ા=׌úe9 oĚXШHHֱ%b߫"Z>x1Z W LG; ۹@GD.6u6?zQ@zD#r:3U93Mܾzmف4?g^(mZ8>,}x>P:3® n+!:AZo8+ SKHԆtҪivl?@6(y^@= T`XtoJh~Ht9p anNxIݪU߹Jj KIebsO.$1*trJVW!?f^ PsUA6 ml[\)][[DdMxWk]_źҾ 4/c'DZE|K&u?zol,FQE{S4Uߙ,M u{vcve@ -&eE2٠+'Þ1KL O'Hy4\QҦY{̏i2^?6y쇫jL_. )%byFv߲$voގ,-9Zﴬ`;5^{5DjJ-SݞeႿoCrEdW/2E4*ɞ:@~nH5=AU@q6YUZ&Iyv PsZs$8=cSF$ >٥|,B  :Jhz(}**OȈS`ǚ뿉9Y7:֤ , 3tՎ\U|eguհ8ݵF~d|bY-> $C3D )N +"Uo|]nrQV,O5xQ7rxD;GY|Bvg+7?- p+ڧ6l QSUHe NyҒ9cJT7(&}?Io Y ?jl!!R%TVIU<.]$`7$:Wa*06c9xN0rv4wN6^ɤۤ1^9+XE"ܠ 0~\W=(E;@Ni$'?mȥSԜ^"ҢD9tXrO"1^'4#X3kEN)PȓI4| thO/AM,N24JV!Q3dg?ȧв0|WVԭ#Yʜd"S,N#LL:?*}:.vHx9楊ݚ<#{U68&~ǽՆ(ZNFKUa#rF?OhԶPcW>ՁsZOnlG.''#f9ٗg!_%of@izSS%bayGx䟵6Z+Sk,R#e9{ [pUSFJe=N0ck8g_SE rzQ8I1mG;x].-$oYbɧ'58Ƕ+bP0*1֭a)II9Stb-#*s ~&@Cu5rL{m;ܛJ[]6=J  XdR:Gsq>za&0de<96ᤴڛg\BcDdnn;5e+Eg>VlhPƷi VS0 aZN{py Os^} Oz.5;YnL"8or5ˉSNjԟ,ogG_ %oEr[#O0S«j93]i& `srx=ז|<]ha[,HA= Ii%v83\1x<3ed&V%ΤUҿ*coVd\NNAM.\w[2/uLOA^0t.oUNˡOq~hw^S*MzxxʛKroqY#Ԅij6ڷ>?2>ӌO|W/ -^a)_z6b"{nE :4ƻ*ӹ .U:?xDZoGw5c{Er7Eq0l㗙$q٤Y1j)a׹h`-/1Uwlt$Rv&Nk@ FW'92sڡ\Pf=\晙yO6Er%Fw=R!>:VA4Y-$Z])uS{ZE "ObI5mRV[rhCKBMSuFqʌ^gLB+3sڼAo"ٽ& sr^msῈ7nM&l9o>xoP>*=AN9}sh +/o_dx<> '|㢵ٛQ.uKnʄsHNRK¨TPg U񭅺22bj%7ANR+#Cb~Ӭ-׫u?_k?*n .#f}Pkf;r~\X 4$*b] DdUʹ؞jCcN:νP4FI?_j>#M|5\=jhu@r9^jBjK ]Rn}h+,xJti&Z#uFV9a8^w@튊̲J$swztdoBrlGÌ硩⺖PJj<ܱ%?'8NP1WF/them=`.V9g;?߰m*Z va?Bvۻ }|3ڷdcQP\@ RNASO,0:כAs,2O2lg0T}j嶋i )Ķ Ѿ*с#͗@ &2Hk) `#evF>ݟ(;Du`ہSZ\#c&=SQD[gJ c9K|E׆,M" P)U%؞ޝdb i#~` 9Q/C'ZS)he{ˏhK*ĸܞzd Ua3 vyJi )p=O^jnsלжBx]Fy6Q4B]?9ǽy77s丶I$ /.cBo>-zA-5Kn],z RWdJJi?hګĿ <C-y渌 esğvB6M{/|Ҿ#N%햎Y N'ǥ\? SN{u!FPOW QIsw)-ktR׊΁Jυ{]WdzsX:|E6)K[$;v0}kF%ܿ ]-1SЁR9tS|SEewN-;x*]Ď Ph>Gl^C_F;x-a%FҬ-2nx[.dG~>(|&3)y~IWׯI.d܂KVR[&c)d>F4+lr PV^ C#d+U- .6?+\ ڿc#[ZNrc;P>FG[71/sfXN#U"/՘ڸ~ox@ xGQ =_cc[%q7Znca#=}kJytdR_ ?HQkh^&~+UDraFrAAR3pOhOsս;E5p?l. w=?Z4teubj9,c'X| ֶn㜨1Z| 2[ZխTg2HFۃEnm^OrH'=p1ZxrNz0֯@Lj6vATo/乔GjB!=tξ3iюGt!Q GiZۥ\|yo /C{-TVð1Tyhǖ;5*)G^_.%ءR )y08i zڗj{A'umB6|ҜXw8^;giʎ0F)@ s:\L QPsz_ø |r*;Ivmi)h 溧xS8nd`G?R+F@=R1AsI1.POr$E 3e,ĺׁjF{XN/ =2N3JzJϿ(lS% 23w涌StYܸ9<ծce}FRXu$q^WyӔ|lVw|gm)J/M[lӂ3+z޽!41++S^0~r{^ݧ,F0v=yU'h c(ӵJT3`iwsӌb:ary>jr;;ԅp R:p}hdqk;Sw@!Ҿ ǜl[:R(;q7|t@ {׼J$AR}ϼ@Uwt@l+q~um68bDbx*z⣒olrzԻˁF1M2:KihɔSDw,E1 O#ژd%r6=(FS تm=j]3mPp0H=$ t5UsJpv؞I\iE019'ӭUP*s׃R⵱/-h^><\?euĹ u#t;(jAc<:_|FZkX'6LG<泄왼.k%`K CWEGM]-%oo;),«nq`[#Ԡx͕Nǩ S3g/h9"O{ttKGuk-Ls*CF299X5IQTdҴpyj$ ۻ,;xdG Snf.c“׎՜*?ǵ9&!0r=iYXvE zR*=X#)#i Qg暲4#br̠u+I^Z֒\l3Ȭ|hz?&9cSxJ?Ufw|#[T^"5W⏊ža$䞞մr鶜ó b`?MXCbhuFMѼ;T֗bx|ttg|E{3xȇ\`W:C#nz$f'z9tD<|O?~ |%{]of@zךx KBOG[&q.z~OL!;w Q$Ìs޵"6N)[x BxCzeC]4>P-y׊|QCk+pEFztn5P8c*Ij##C=hЄVއY⿋/5C@b$w D 9#-"d08q3tU%su%ƃ,O;'S ǧ9#қ}>[3{5崾}bǧn?Zب 1@ x⛃nϗq276nhV]o s@9q֚n%$^3Pɻ(h9$ Lֽ?`Y{31Vh|l͌n9Y3O*9Q#*U#! ?S;H;HܬyAwXv-=[|?Z_H&M<  $ܧ RI^&V B`kڔ7 h tkOym[˓cW[|"3N-80==)ѫ6ьf|iKZ6Ag뺌a"S8^ꠚ>^59o71޴~6E̬_:TU Sm%cXU9Co 2WOmZh5ĻW+ެ4GCe-"s?ϥj;l90+pX zi^!aOn F:ާq銔ޢ AtFL C!X0OG$|㨬ēt) }jϖqS)%oPDי$^;Ӵ漩'&nQ,dutctp{/۠w+u#<慗rNw7ˆ5@WW תx{DE!,1ZŠyzoScFEHTqzю0OLS4Ƭai2J ræ*ߑQFNztYWp?GbD!}ܖm3=-c 9ϱ4@@Ҩ|# q k;l' zLE ߙziiáׄſH,Љ'5 oU%?8'=JMk'qɮSPҟJҝZgKP/IůHӵpFupGxKIFfq]qշO ͧ]2XmǷұd}|Ϸz TG^K@8 9 `C*KJ}szs@c#,@9 NWf2[$pFG'4E@ Aqol8ܥk+OpDvegUn <@ڶ{+]Jzԯ,4_-HUgD*sHGىҮi%WRV3`ګ,eA.'>%;Njvċ)ȞT mF=̇xp9ז$F`ߥ>SԱ޻iMZZ9J۞@#n5܌d,7tm/%P-08%X;Vu+hGo&ph}nO`3~A+k%eԯ+Po$[ +֗w8a}#CI">~hm}k]:Hi"~['#ĖUDd8TJ.8E%{}wO +"o@=V4wƲ ^!48;NG_J,w Ea冥5~:WĽ?TR夵+j[tU[C_6i.? @b #N确=-#~h| q&0x${$np8^s|O0/; ǎ~A11#q:ֻե]Y=͞$_[*bMG;@Q2*Ϊ$IFI#" Tyj!@'$tǿ:[$?JtduR;4`~iM3>k/J^:g {b5 }]O#95/'9皉؆br3_kzJ5TltP @Ʀ:aߵdg<7} beȐQjd N}qUD ^ށ|;q~[ 9戔䟯ڻb 4(' :4W朮?0)jʟJ 8Қ2cɧ QN)Y.)kq#V@<j$VdϽ[oo%ԣ95bFۤ*ndqS|hROzv ]ɔ"ϩ8IN|9Ч9-uicϕOv4-%ܰG$0¸8fH'?kapJP (VVqJI;/Tv >{zַ?ucPBÚIoʵZ X|nMuSUl/܆?m&5 WOz4ρ{=9ݐMס?ҽL[v+60={m'!޺>|tۣ/nnn»1׷S stRE?yXGNfRd#5cm[H?  qW%ͿudgQ{Cèsμ/vH- #8 `El|Ñǥ\6;PKmUv'XR*ԊZfe5A Sɪ::ha I V׊Z—W 6yvWbx}j\֋qѣ(Go36{։ 0,qz㭵f& =ӚZ.OR@92n7l~c;Jcvs9>I;䐻z?$c !N;R?XHO V:RJCB@jpObsT֢Y>h2E%,6w䒹I2hLhJ =Ipc‹➇ \dА@f)bAO6ItvFi-H 1/ٜrץH͑ys^)PgQ&*y;x ¥ۻ'e뻭)`} RzXJJ:&.ڠ?,p/'KdŒh0(3p+|zo:t*jCUc qڔ! -v0Y?oΘ"\y$`zP9Fi@8=⭾ýB|`O^5*Y=G?v+Ĥz1?RеʕJT$,!y#Ӛp\zF#aR:}K6.de>r[j>a`յqkKUd $]T^f5ޝ=^ao}si,z;˸ # QԐ9^kUpSG?cJMձvHm8lRF9:֮ž /$,U}rMqWz֕+gd;Wekc΢3񔣁ʝ?PH]ņxƟ RH1=TX: ?Kbm,ػCO1cO-!+UqEz¸֚_YN1k+%v-~0F³MnnoY|mZś_A1]{CRR&R}(\3X­K9|`tȊZ[Bp ݹW-~2FbK6FDQ>s#7,ܷ=iL '$(ޝStۭ!#]V.MGQ,ggqӏ«XxsPV/yPxR vVmc<{W-LUY~6LgrIݸ'=+ ^t]7Cs~|^6{rC*ocYk)}+)?|j;OmZio}&U=K:ڨ}?{?sM B/Y7艔enߏOEǙx 94oFl`4lٮq!% P0HH_[՟Xh65t"fU"Oֶ/3E W:?ʫ#6y.1\|5? $p|[*ObiͭDTT,_3‹ffp&ߐTԐNO@w~Xwfi8 S_)ݳR2ym<JFrs+z֩wC# 㷵h|)r^^Ǔ"OUOKiGGb1Cs1P޽CO;o,/;s?1hЪi>+'KcĒkOTִ-|ݪ3Uϥ9Nb[FVC81t/(^@aяBK6X;]rMĉv_(sBD}7 ԽY=ca~u] ˑtOY<t0HVl}iCa94Ո'k%4rx! ?֖Eg;S2 }ҡXvhA/AUʯhmB>7ՓCngt^j'۹"Xe36P|=3O(I;')3ХGG #<ԊpzRRĜdsԞ[):qN/z^Eڡ 9$`SǛI'֚~@q cږ&dcZӎl9)ED_\DI2liJ|a^^7<< &s_תᛱ]$ZݙBnyE'A1FH8`b#rHj=div,F7:觍&xtP{:؎kGr={}*|0[MwV*OҼamgTW!#פhzjz"zUkNK<.bj6"u(,?JaynjmݚHQ8^)=X6xg݁ \p$";Vqom?=q]M }36%|Oo b/59k5~_#W7ƽioM'C dd죦<Yewii̓WN? ;+K^4& #(# ;S V^=#ͭrtmKH!pF9s?Oig?K[\jV[c?QY5+FS89\ZXy)Z}:T0*kW'b܏ں [Q%#8)ZZĢU>bHى+K/Fܷ!p`Jd 5<)Fl!Ofz4z:Ks/ARx瞟z6!ϲ!n'E$ЂYJ0vA8҈ 980;>خ#g~UJEYz~_樵}Κ5c?Դ"x>ޕ ˡ lKf/z׾ݫۍ/g gڹWBd}srsd?e|E(#rs`;P2pOj~%#-ԗV4[y<9?C2}KKoH/EoSNAx)8[fVf >#Ѵ=mlH.ˀwoi)I{~b+F/W>&|rQh#t^ZJv<*=Oʆ  j|r g#{rci]@J㎸4Rr>0`6팂\RrԹ;*FIMHv'֐2l֜Zmy HJw~ՕP`mN0CJ9R=)Qvf=yVV݁I:Ll'*1Je>O$,ݸMqȹ|p1SU ⶧R0jJkS럇_4O"]eJ?h"8!+"ܜ&*ҵ+;.FxC{ڂP=?⁊i HR=LyIһiW(KSň.xH)Y#j8Ejr@VHG Ԃ;FO9cgsOsWwc'95wCwl$UP dURGSeQKS>G&4A 8ֳo<;PQ3ǵz=oaGLvzbw#YKRl2:3ڳ/5=/$f S=~4ĆIA=?*ϓBd\5N;=Zh oVS E ;~5:Ë-aH!~k> a-Nsc?`B;Ŵ'l66ɐH ݏAK<$sZkֱ ~NNF/Us:ᦓh}elyVZVs!k/ h1&@@@}J~4>khH>7+G5۹[%c5_h~qmm<\(\WoGLn)/e?*;"=rH&/YDiLrJ |H< ʎWi|׆7Sv|S~UilȔ",U>񧎾*]G 8nB*67cWt/قii ܂ӯlXy1ƞZEFҩ^kƌ.[ϭi*󩥭苕5)Ÿ x<r^}o`J *Z—2Ǹ䃻Jp'Yw'?ڴc)тN. s=8f^C4gzSntk[KWT #U޹+_I-~B->鿙H`5 #ZW?s^8] ,W2n8#'[ͽYy@3L瘵/q/͞g?{WLkB:!IS"UST CLuE # mv"< &N+&™.OC^ݝ{W|9Ro c+ׯYnh ׊2xhsEBGnX0W>b123O9ڠ@f [ ޟΤnyʽWh-JA\.kCH)ʠϸ1{LT1n9sER85.{ $ bʭ#LiP760yQ)zCu!'s[IKO=ԫ14uB5Te{[A?&3שٻ\~ 5k?<9+kS]vnsZ3Sn`֧nbil#LUw}k~&ij7pXngicN|~N⯋R@Piϓh16OF<׺%Eұv'kFT(C2LFUYXn7nICs8[BGOj. `+|jZJD *{N2E*``tT! \tQM[TǶ/) N?B7j0FMKki6[9 \&\P$e8HAM $pxQ Ytu3S#NX`'TQS-DaZfp#(-$ g8sҦ\לzjVYsKSӣNc:%HMXY Aݨ`0}q"w0O=2+ JږIlJ8Nz4A#8=*W#?X \Ԏf$<1*T @JoS9aQgtNf<jfMň%O#2<5M+rF$):(sǽu#L|rkyW~[MǑVpJ{~ȳN@'0VwԱM ?~`DXc1.fKFF0B0y=x u +:/i,7Ԯc1!޸Zgݽ'|S]8ڬ#)wW 玵IK9&n{s >7: l8+i|4 Fha=N8+tNWtEyW<O+a Ak_`4Ug?xC{U# Y*A$v#=uӃܢ̧tgk˩0|e9$?Xնn9=OCq?t=Ozd5mP:V-" lCyqmDDp[y,vNf=/)CQ}P A_)cӋpj܆$=W;a:棙{DЦl6͌89OKy^~o.!򷤙gvJ-q󑞟ΑpzN$v=)ibAc B-Nny xĐ<҈a4`rsПƝX1tj+@bHq)u#>NaTHgFQR#lt8^(i҅@KC㓞I#ا[Wn)s,yrNv℅J8枑nv~_DNp-"iUmDET§C>`$_B>#4)Ir碎OҪ>SW7Z$+zi`S{.%PrI٦ɮEwqWd^"c,TӃPR {Bww-e ֦J2w#NjX 8?(N>ǵ>;` (Q[j,9Z]V9e8 k Ż-wn>niHU):ǸSt9#*R^0 l2M\i}0%uxP0FU|O-ي (=ӊarYb*r4@4͑ڛwu nfw5nK. qZmP*oX"TA0GWlsE} _WΫu'~VVQZ!@OzrMݝk r&Ч=ȟ.dH3A$1(Lp9$3*~\u}:}$ccwJ$=M{O'Ƹ5I 69b9ZxwW:e%{Pa*bxfΩ2:y 1Vltk?^=[DD^4J|SESEgI:+ :}kOA,<: Wj?-6a<nr*st%KUBs.g2q z.X.?i8١eC\JW{TJK9}k֩MZO*[t=#/[ 2==k;Vxv6:ާk<^k_Zu닲GRATUăkO_\ ᩛEeޖi.t/o|>\LYZ1̹L )Nv{>ӄcegp5cHֱpWJi%Wva^,];F_1$rdFΦ@rc,yS3u1jF= gq\BQُOck/Wo2!u5{ZSE&L5^_\x;rԮn nj9{WMu$7v=ХXJ|6'9b^3`+(kl76q?ϥQVGٮ$~q/gSаx"jUnU=ZTҏ,IY$qOsI1TjD; RAbgߍgZ:6\RM!蠰*H*vi 5cS }ҟ % `E$aM69x݀`vQ )IST6u2`ңЎmܕQ>h!WMrUr)jXb~ӂih6ޢC.rz%bS`@#*1dVszHR*Oc֥ESԌCyc$[mڪsywpqֺ?؋kp=N+Ru#.R G/F<̓Y(%/u9 qD3}=}IMF>M$xi2MumrO~o:׍#O@d7_a+#"/u3aQ|y~1 ]&lLϮkasI)/sWc6EfRc̹FAWw̶H,x")f|fs[ھ^RmA5䵒/t۲*1$o`NAߧ[0UoJOVr0{})*xFjAo u8#+=OJFNCzuRoc;n⟱==*9'$Ӟ(L(rZ_?q~\)'ei7zApO+'{c5*1ƲṖȅY{`>l. 㝹r~bҾ 1gDomXL#x8g!FH9`P8A`:QֈpD:ҝlʨ-)95')(-{Ii.OҸXH^w&iq6yQϊ:'M_krGu3q]+`fu ; k[1Y}+?zQ|Uh8Dm0<^{N8*Sg`$ق@#52dN2{^hѺK(ѶUzs)SWJBI{%^Y$#2fUx21 K/96,n9銵  {Wu qyi7}۫J ` N9OPtcPEI=)H1zD.;0&-+A U% f9]?r |s;}jh+61҄Oӽ1m~΋h3Ot89?4 dGҡo2%VcgsS%.i9KD*DAz ycrx#λR%y@²ouYfoݐ uoezU*7265m35=9s{S8YHfIhʒyyQj38?\HtS %`cui$X19bl o;nTdmaT7ڌdW9,:mZ~^H{󓔭!HUͺmߐ֔v١16>ۻ;=b_]tpU8)GVI6d +MEȹߵW$b>^:z?}H_ uӎNY=,RIN cjP UɎU|1$hބ E(% mxOj#dr\1+jM)ALd?7SFv)Ei G.,2^*:4sQhQsMpf!(Jt8oWN|9dCBD=jmwCf7 600+kEKA^8QSƛؑݾH$NEu6-";#]c9hlp'VGUp7m؞5O%(• i(8՘ ਲ਼0%·YijjX^EV?-dx<1AKc\ϭm1꫑y3JP)g|8D-72'oa~;LJL%V×c޼ƍ_RIV?$lK3m:i40s^K/ ^\Z櫢|A^%,oV8z4?5\&>?J~ \|y=JJ|FA\8aWVIr>~.1o[El ͥp FN=X6%؞VozŴQxN+5S7њ$ m&,cg~%Ӯ'1\n̠6plS>>e5?]V=VdrGOκ_ ĻLK't bOӀN͘PW:B:'5Ȉ!\|W'8­^^U؃ 54)RVG7~OMy"?n| 6h:D)w"_̚_xm4e1'=_l{j?q}d9$' 秥"I.G t 8/}%6hmEzz|Awk3qNNr4H+I]_c;6>xRp)O-#&"Med F8 j"Ե/WX! 'Ũ :V>?Z>)'/_Ti*[6? hV6J~H8Wc k׉ߚ;l@ykߜq7)'*"N̸ӌ2rUkKI>J،M hxYyE2i 9?)XIZ\hTfNrOO"@jq.>^r*JHeVn0p:VďNx"xPq^JN/V+>`u6Y \zzwm= Jq8)g4]})EU$XB=X0=3LxRgӽZnlJ;4 v4 r9v- m2FaBWwg2yI #8LDK8=*$?/#ϥQ,bUeڳ`)AYf_/.hcl1=@F6x [2zUax"'ةqFYk/Q wA5y[%H't};g"]Rq,p6Uq6eP0t~ \ ZB-Er <@xS!.cҵMBr 6]u}V{'haZSס H.PޚS9껮d>dġڕr15Cqzdo:LQge$@ gׁ:=ۈ`;S$bj`(`;D!@v?M;n8D* cOo`=WL~pHoZ Yw+mLw(9뎘Z2%1#9',>PNz̾^X?E>zJ|Ϡ6օ>SdsI g8^۲1u W8Ux?(2d1# gTXq'{qR GgM~D'ʯbڝ{S}1? #@KeN8ӷ[_IF{㶭O.е2Yݿ?p7ěUN4֒q%#$s֪Kr.jk"&Iflr7|A_,TQH4\WhOv݀Us{UƲӱ0 gM;3{SDWtyv<,I?O?z{aP:p;8>%\mpA\m%s\;28#QGMpBYϚLpfl 4KrL%U&E˭`AH$ +̀E\~gZ}Am+`]1-I؞+h$@>WČ zjRN꛲OUrv1*reVMh>VBQ:f >~ iG8Och}ؑsֶWZ\wCEWq9mt';&*A899<ս6UFgl?-Wp.ܑǭOM"c<¿`kb*~gu^?hG Avz_هLVh`A=-5Y5RZv9eRmؖFCųJ5 ͪ<8 Y֖B6Sعy??v Vaxy`c"RpӠZPP3Ozp+\u+0Wnq -pW̭\z+7|r}ilCuYG'FJ$@ܽm\>X=6ץSi"QjggPS\IGݑW ۨvc5N஑,h=qMFq5b{Q> 2Dㅫˤ wҮ@e0~nꔿEܷt _A 'n8@|uz[kBt(d#=kB%n><MZd,BHڠq< TuZe[O`˽A]<^]Q&*l\`t%K(jJYZn#SY=GJ5k'B- _|">9t7ˡܲFvȬ\$qgjY&`2?bgrmt=z"H;9vr>=>+*eR%)0G?y;gOMpNy֯U(8ႤwOTf?_]zpyϩKێ DF?)&YI#z&Rmj۰0r_Ƒ:~TĘ ^ &n[ap qrղ{8R_ʬ*Vc^1POb=798Ti[H')ʜUؽh 3"@9 (7d#TRᙀ,X=X N$ trE'W~SGZ,CrOX\21 t 5zkω~:7K}k}qּ`Qԕn(- iV㝛{)m~gqzIQT8$jLzqsFw3z$xS`Aac '&c/i&rT֜o0 9x=*A93j}YF7{ x# 8rwyة|g4)HہMͭwb7s҂Y)z|J_v>>+#GPZ,XԲH+A$L=2xRekj&#:2A1QْiP 5L [ PRV|/=(I#|ӋiC/ Gn5ı@ĝ6}oDa2AT7d!Q.ۡ{(9Y>S##Umd;_Fw&xw xR p$s<1C~#+3}ӎJ2e*aev BV^A %̍5:Clzl*c9SֵRenWdxI9H<kpr\ sҡb\eAϽL;CMۮ4۾WQy9"WH}.T%sڻWoMtc. u)<9#mYcr:,ͳߐPwc뗞ci%a¶I; 5 0m1?]&6TC\wstN'.OsZSi#fax~JO&1p&PXOx=+~+l됿ZE׮+4{K~'g8'#.~ `kbH GӞź#x>oSY{=F{{`*AǒN}(дWo{TB@N+⹹{wIEbp`byKG8_zO8>> ]n8&( Q5D>bjr~Ps= 8gϩǒvZ-,FyK[h8VU8ZY1s{j<Fz„lMy8$לVNC>I8PrO [}ޕ]f-i3* Y{ፘm>C!Q ~Ѿ)փ'ڢN`W<\.?_ojcJY1׽KʴZh~)ԵͣT/mė Q\Iixlmd~TˋSګ£KVC|hlF{ӑ$DJr~_׊val>`rF1sޘc(ڬY0r楧{ԍ]X8cs՗<ի:~Τ䎵n"= zppD_'W^rx5%ĊoNz:bA%}*%yn%M)osP2G@}^RO#j*O'>b+$;{h澅Hct(d%:TIr7rǴ$6@8z'-ؤbRyFi1atC*S$g5-tQkbQs6I<6aW<ش߸U.[bgoZk9 OinoT2L$rWЊ)@XN qRnba?kt8*d5?{qީ8csŠp 'RJڢy3QbM'޼ҙC #P!}NgOEv 9Aie{~\6gs"I5{.ܕ^Q#:tHc}Jk1gϥ+NNࣜv5O#k)++wf|l}#\qލChSmrxV!zgOJtQ#N~(f+&AjxmXcAb.z]?0_ʦN6\fwBU VJ\Ix"$`~(N*cme\,L+'?א+[ߵ$|תԚiI7ڈ # ׃ɧ^k=}¦aL7g-P芒˵EhdA3֘`AeLMH((${Q@Cn^hr2F) 3jcMI~e12ۀI a$Z t(؅kG}2}e$0)T``S隤èylb;22HsJc~~蓞d 9k{r#1b|/.b+34<1'WBkx:G-!p N[ax1~LcuZkyFx`5_T&F'Rr$<3znz-kV4w8?_*K!O6@ ][*C Q!zRd;Nѳcsd _XFpfq$.$]BJ,oC[ Ā7O^zWe;.S/ibrzzW;s1\f?滿ZE'rz8 85VfiԄz A'f#UMK s׵(Εʕ l)$Z#m7ǩnt5I7&SDѿ@3J7̤SULJw:P䖆\:g?6sЏQBץGүfzRij߈qr:)#h$oucUn'5_Vfr+і${Ԃ`_8G\SnloUxR 0\S~%HF6AJ6 KzzQ$g9<ҟ. #5+V|0v͓V#up'BIG-z@0;G{g?python-messaging-0.5.12/messaging/test/mms-data/m.mms0000664000175000017500000006623711707534031022273 0ustar achiangachiang0000000001=Sgoldpost@hotmail.comGOLDapplication/smil Text0008.txtAudio application/smil" Text0007.txtText +Text0006.txttagtag.com/gold Faudio/amrgold102.amr#!AMR 4*p @j CŕQ~)4ߌTD49/mTܚa@4کT& 6|0 34kT &U j:ute)B4<>ARc]#[u;P4T U4K0RNr~zs1}QZ4xUNyfȱ͜oK{543aQYh['q줓{\/#4Rf_bHIC>XEEJˠ4X5/6XruVI4!AԦ{±D?CS4J;,PVr~ 4@qu+8RLVYU;`4x!|1vl24 +GZCG> pm 4ěO;&k}!б#C7ul 4Đ5lGJ IÂ4Đ5&l+P@=%}Vm4D[6dK![ϕJ"T|4@ejpCUZ4FS4DtvS,Nsxr1948[6oGe`sfYû=@4xl]n!yI $ϗ XIp48<M@_ćxIn"45v"r-Av @4橤p4K.@.4վۨy5Ny'Ӿ6Ơ4D|wULCfRL[+4u?6ItYv˛>046ftpK+846i-&P4uԻBAsrzG1p04uw]+dJ%&˲`4gpSh;:K,4Aj K4[AC%A4A%*mEdhp4Iuԭ=;BOZnLB1&4{/eeoNnU$4oޜq &bBp&p4u?(q2,>Ҁ4,Xw 4d~n3s4D>ݿ=>Q'xP2}$W!OM904 ߞg3QhY8e`4,fBj%zo6p}"?4Lg%6&~y4_{T,Ms 4ە~6I״HM24 t q?seΈ˹h4v0^fRKlٔ"XE`4؄ߌu7 6wrTc4,~70V \٠4,6 :5ώF&tVp4,eޛF&] Iȸ% vp48kL{;踟ka0mvJ<4(l;FzY42b@4 3v܋.^#mBi04E>6=v fP /"PH4(>=!.29O5i4h>#:ЦZk$0H@4 3>6fW#%&˪J3h4>\ǪQѧ6ݦUIIIU643?M!J gzN[4v>~ܠz `|eא4NӔ`4@86%jd^TyP (Pf4ݐ4vA6ۋ `>< 4,kfoH^ԍ KG4b!Q<ڟ^ewQgAuK46 D\@4k>Lu57,%y #AP4,kt{4x\ޒ |OMP4,Q3^^bA4t<NYW$%x4 6<4["fM(My6L/Z4^թ>ǯsmeBp4kD6ޡFIi \ 4r>mdKo槄X$%n 4>܃=O(?]ggŷbR,mY@4J>6ثJ~l[Lw~>y[F@4>98 ~\WO(kZ46ܢ~,/pm\4!? ںݜp>`B:̓uvA4 )60;%͔ 4bO;ps|CD$v4 uP ~qAUp4D]ضxNmX[S(ɱ.} F4itLdTId2I- 4DN6DO/,}T4I>vG\ڤu"9^s5 4,k>v fסlVP^"`48>f ֭WY )_4t 6ޮnP1""s8p4bL>>ۡnaϭ SɷB뒴4)f>e6%ҭH<rAi04CBߙ=.y _녥4DE6ݧGYd;<ڨ\k/ʖ4,}7701i)wZ+bFCc <@4fg7ƘIL8uج#ݰ4g>>ܭH_$ `1áRW U48g3<'OL:u0Ixqg"4k>M<#p[֊x}  B4C6kۛ}:J14XGvpҡc#c5"4y<ݲÎgLsfP4Iy\xqtȔѴ*>w4yV+f -eVg$04,>үPA\ߥ9xaw+FRM`4IvHK6CO¦ji 04,>60 pWK7ڴ]4>ݾC_'z4&'4k?6{Lx'R-]o0 4D> ܥ42[MT6[Kmk\t9;)uM@4C6S4ᨣrg̈4XLC7܈һ m3wGu>dd4bڊ0 ]dDmvL04!F(Aɻy:3؜"O*E462GI7BӇ|a4 7PlO*_A 4n)6*;&YG\G}XI!G4t>NxzͺwMP437WiM%fɁة~ 4k }<ݓGAojZ / 4tV1}11M(z4bD ,6 0#q?Dj}|4k"dLwzp$}P2Y H_4DkldZL ',@OE4k!s_ufF:l4E4ۉk)BdL_ʪ4b=6׶-Bzvã]r?72:keפ`48Q6r1t5A[Eo a4̶>|(r>I^栐\jgY%<= 4k>'.Ѯ7:3iLJ@4Ҿ7.~=qf Is-04bE6ۃ.8): KP4 >=2ωWEI>S 4p>EۻGvTfN|ǚ6@4{>GȖ9l/K6pe&184uܚGrQ*3,q74k>NևcHB2ljl@4ikQ;6'NxZ3B 4II>!ݛ.AE4,Kvp)^ДD`4 EvC܀ɞ \nA+*p4kB~;u Ҧc!]Hup4bCvUG+ 0SǰGV|48g"6lYt87Mr:Pf4Z{4}[OȩQT4g[~+(sҫGm┱q S4g[6C#UjCŸ<04g\YgV_e&)!K 4"_7kWeQK+"MDp4[w@_o)|4L>>%v+l+n\4 ?u7*44OH;P4L3F>߉ Xler -V143>[6G|Rp;|/Aų4# 6j=&ꩀdH@4":uI݉lFA]p9p4"t>Sv NJtu4,f7qa_VDRPː,Za@4D ْ5MƘNǿ4bkD((-=Ay44qq/0YI];\4kD>g(dz6F +?14I?Go 5,fƶQƪ:'4E?~[sώL$\p`4]ވf;oWVK;@dP4,,7ۚ8 (xNC4r 6#&<æ dW43<-hR [* wu4>6$#>3}2cBn`4Jn 6GZL'm"Pxd 4DL>vHR,ì PfH; `4b>:O2")\ZP4'x7$%lA"Ń#4@g>7 2s:jem+evH-4NDq&a{)iU\P4v6 phc\_n D$.4*КXbSG"i7T4,#?=cKY/>4>y6ݖ )LYPo&а4bv8L-j)߂yTd!)e04vI/7ق.\oq )V4356؂uX}<9-KG>-Up4 ]6ݥu$x4;Lkn3f<&`4L\ڠNœw)[$4}L[59}2z 4?v-*UGrMy4t=_4rag 4,3vتjL R+$K6{44"z 1ԥzZP4f>~l'pE~4f>./Qyuc^mp4,!SBͶs,@4)˨)4Q=6)׹/ـ}W'/m@4i!wv ͋'Dݧv4O]v؃BldnN>\``4b@6I vyl=@l 4抾? :O7! )BWL46iIY6 3)V4bXBg\oOUP)4>vO Th@nc-mhM4b>o2z|x Z;@4k!>ݽrb xn4I6 8JoVQ?+4,>f %[en`4Ik>6ڭSigk[%g4#> vܱtv~0rtp E)G@4,q6Fp;[ͣm? 4iv-ZYWS]4 6ސ4u_i^H!EK4 !`؉DO=vj0z fb4k*se]e @0<`4k= ;$}ItTɏ,hP4\ ?QdY;Vgh<[4r6ڄ"dwFޖ5,<4ڐ<$4jd說/b%p4 k@4LFԑV<פv!j384ͳv7Tʵjfv4FGv wƧfOnP4y=J-C͟ 4;6+Gq59Q5+d4,i\lَu#N&[*2CP4q]m>'1˶? P4*I]GJ{Ѻ!C=Sp4&F;Q7FBx4AէU>GCb-&9LRmy4^MlV1^VN Q[3z04ɟ'Gfۺ P䨡&P64qknMwM\x&𨭙4&6=d?,++7Um4*!6<3*.6v(;| 4<6fd:ڴ7o:a4)4J)o@O:^-4_ V+')70SP4>t2PjqM~٠4*%bcF$}璅DJ.<|4˪ pv/)1S]P^o4BMG/ըf~0L04B&n,/nuنLFT#`4B6/*+iL*;V4D\3ŧot Q̘`4"7ҹ'dɂ2W5fE4<6ĩ{9.ec{d> 4-A>G{u_dKChC4iphb=︳P46$RY*]`À4r*CuH# 4@<)u ^kh%@4SxVlXQ0E~4iH!<6'>i~C`>4:0<hVΈ@4QC7n|?]3UDx\4crGJMS]{w4b7N$(8`%ynB4ߣ6ؠ1l h 3 [ip4B#P~)CЂ6*4"yktD'b볓p4"ڔ]rRSh4K62ޘqnaaLu4K>"MQT@4Kxr{3/%4M5_cg!w4hU>iiˀ= T0W-4[69G!xqHB1 3!4l]7.qf7MQNR%P4i]7˲|hI*:~-4,De7>;tihENBT4Ie7ڣ,*`@=F@!T 4DbBN?vjP4e6u? g!ktbp4 3rq3+woYL4k3e6u ,WÛlK_Xfe4Lx)~v$߰bqʿ%`4k6VAumF.,#44>T>(tlh64i6ޭV^.>4xwl2bM4<3hc-"'b\ҷ4kfi6ډ=.s8 `4gz6vHM'C$}Oco04!7I&Bt.@UhP4ifXR.P4kHi>ɓsvfo8 +lO4i6ڵ YrŪoF"M 4ě6ZYks2~vL+QB|Z4IrN>f敟#V_2F9sg4/:ƅi(*F0tENzs4kW6&G֤Uvp%\v@4gOҶ"f&uzB`$ \\K4@h6*N 'Ԃ54E}6N(LɄuPiZ546]"_`@ W@4i6ܪEru,䐇l40rAdJ74ь p4*K6)NMqi.:>)4iGW=7kA^>04lG<M4 ڪg4ܐ>ڍj)_['8]J p4ڼo6⼟x[M]t|/ 4i6G/WF, w~C<4ۄ~\<50uP4Ih̶)~!8Q鬵,Ň4iJ߸.4xmz)ǎ 04-vړG Î W4HigoKQ*r.=+H/04<餴3euU'&8]M~4qD9?S36mstCn04k6~.Z Ը^^4^"~@mEP4g)qݎ:;c40D.4NP4DP7==͙1 oĴ]`4Di77t@Za&>~4H*l<+q/ ETk;%I ߀4*}>yx)F7ϣP4ӣv=TO4(|p4<6&<`걭X cP4&}e̩kp))؟1p4zGK^}N3߆" zk`49ڹj= Bv J<"4k,U6aGJnQg;I'P4B3M66fnh$QwlLL4gz]GUpʩR5t[ə41J6N1{ҝ&>A4R[0 X,3XG@4,,tnwR7P04k*Qe؊XHY]o B0> 4,k>LZgi|bo^l 4*ږ&qs#'̣P4DX>`ֻ?LTF#4Xf47ڍR U78ACL2v@4fڒ)^ buqp4*M޳>6cdB%PT4gEj U,+p4kj4ח=[qѷճ k#> 4if,+d]&4E\Z P4&%jDP4ڢ6&=˶KkC@4,hZ޻#?zsiw`4W6 Cm[nd zm4SwPP4Q*U}RD@4K\6dsJ|M 4]wފVݚX #e14]6 uO&Dt"z$4v]6؇`P(ќ4Lv]6%Detpe|C4kUls\M_}hY 4&]#U6NP dDm)4,d :|TwAf@p4k^:>°;|%^f47ā1Ng~7t(`4kxk0uBPp[C[2{;4#}ڶ# $ ~4fPԾې5pfz_p-TSñ4vlvڍV* z(б_4HQE6.~|%j:=Bf3#4Ą6l?ϻepq)#2qt04,~ 6'FcR0ŵ4vhM6ہRji(:e4xJ4"6e ]w2g7?M6@4I66Gbl52Nzvp4i=vހ06J޸6`G RPrk4i;k~=R7Fa>Mx4DD)6ޗCJbL14-f4ě-]6da| 48* jtF/h,3'S4,,G`h_Lp4,]6.Yaq7H{gKI@4kr#6چ&ܒa|җ?W!4iE\ߚ8߲faUNT44 GUॏ_ Iɚ(M4q?&[ga48ǜ1q[4vVQv7 I Ms;`4rS7vX6s&ɔd4k?6ت~"ȵB$(4ir]ߓVC9L<i|1 4DZWtU_Q|,4t*6߆VPmE{ 43ss4HR6Lm:e;4~u1W~x4Q5>g@Q>yU`4tewܬ=H_; ط,A4k*e~ᆺe}^*-?5Ơ4iva7AN]\!X, 4O4 ΝK5'~=4l&pt8dz>)"A4<}NJQ4Ca6KLwy4*mל;=r ِ (9 46f+5YEP4}4ոS8 12)c@4J$h>dM;9E`xP4˄z~rZV| 4iRޏvXFgBmp4U6Oe?%Xa3 4Blܮ,ix'TAǂ%074Ͷ6o @s&64ږK-7ߋc9<".' 4ql,܍c_dW4!=yR67.p4AUi~T;@Y!ʋsĕ04H6 ޭE]DYt_c `4B?;|x kgn  ,jC04x}47L (bw5<Y4x=Nbmơà 'Y4xAטGy\T]!/h>{ 4ˊ÷xnC4ӈq{pe4BG4ۈ?HR8A,Tp4xk4ߴKdfPRd4xޮ#$b_OC4Rch4mE+4*k1Lgb.Z5]6P4Oyv7BQ6x׉]WQZ P4BN>}Cv*9`g⿚< 4BL"BU\~]!t4fDVPA4<U17z~m2/^# /ԬX4Nz_ tϱH4ړD;;S.aFV #@04EoܬnJ;D !FOVI`4k6Vz\3"B2`9Q@4d>ݨ~wYÅ/ff`A4i'?xW;Le|4n<ܡs.};E4<<;fj VMg&&r4g,4;S] i9"<`4?*®=l4k4Cf~?@?P4՛6~wAȞ@4klާ̰c(o4mD.:^Y#򫕌\B@4BM&7:Zt7&Iif4@ 6A[h9W"|=4<>>فZ('i46ߓP@2/"dtW<.04BwY5 6'`nI4*U`Mb$*Zbw94N%6зsu–MdvW e CI4BEޅ~ d Fond@4Be6|ZMݴ2Ĕj&]Vp4B-K6d~مn]4Jǧ/ &74B@c7~S؆*,+Dqs4<6Gb3o{N,$4BZxG]"~ 7I16M@4Ih4Is;/t/q}4v!~KGJh(3;n4t*Rs%ȷfgV;4F=4WsO*SxMB2fp46ܪN X{)Of4r*fڟbQ㘁3<jB Mp4Ƭےy؅\*7,Uҏp4,ߒNt%yt`s[#c4kkef5ߤs6!/=g e@y$4[A1@k5L!ð4/Q;nl6buȅ[@4iDS֗g_7Z;ب&<[l0E2a@4<@l9+d-n?h)`4kgv:YtC& ET5rSфN104Q&F$-:nAsJ 4LCkߘS1 uQG7{p43?1G|/QgHw1C 4gb )T$(B+=Ӕ`4<.dUne$ -g<4r;A&@4.ݚt5r<=t<Np4rt5ڀ~sPPݒǛX4i-rz~8S"+:7N04xH[s. pvj݂4t#7 ^.WT ^H4N6J_SZ<`딑6+Ap4k!e4/S~ Kr_`i 4Gv~|]})SBQ |4BO˦GG797=|O04m6܄Cӏsʝ Ǩ-4N~:J:94<3/;ypk\:=5I4FE~nSW} -K9Eː4ϥv SX;$EJi(ޟ4蔬ayd -(H}4SRB8|!z((=t4<-E%4BUX4Pʝq"OR34BSk<ޡN#P)(7v4qAu6\Fނ e(0~ޞ0p4BetGi&:TgiOq4qt- m1%1`4-z7R86i˘9$[N4ƫw\z[n6vf.4d6&jο_"f٠4}2-ED8>4n+b/ l쎐4*lc6̲3 IGa&?w@4ef+\f⧔7 _@4x]ڃ~êX(+1Lt74c6nG<]RT rgπ4<fސ]/|&F!0436݄8.w@oSm/p40`(81w!2Z[ሒP4B̷Gk+tL4_04Bv,a *KLkg%.04@4/ր:]18߭eaU4i,72$&9d˙PlZ4\6ؒ'h|.q3-]f[P4i3X=#E(r104g]6؍-nonlxeCp4]7 ǰ& %h}IWP4iv\;څVG f[Ր4L3]64nڐ>D/}'P4kt8ծ~v&g Y 4[6s໦ݎ΁4L]6܏V/Y B=f`4[6c4wKI7%z4@5e- =x|^'x;Ȁ4iO6)4ޗZ=#YHl4*]!cIG@,.و  ʔ4MFVa#뙣svcn.Z`4i*bB>^kwƿ:E@4S;6(UfF fːCp4OߊY3F!yM))4<6A7gL#K"`{04ڄb<۟jE#EQb4v?=6ٱ 9r0f_""N/`4!he>4•/I!r3 4i"Э6ۧ~:6L}/gn2P47.4e&tAϺc!4Lt3%{tbnQmSX4 6ުuiۃ/;@4>+.7Ȑ4r/}mu05Y1P4~]6:Dj!D!􌆼4P4ԁ56ۆV2D|/gCijw" 4bM5>%80QV )@4M57&Abd۲]H4LO=?$^4;`,d=4IL ֥ Wj`4,Q=>ܭv[3<4k3c6eZT 4Xu1o2\v'Q 4LܓGd<'RvR"nX4(7JG{RTA3NdHrEG4Zd{h|g۹:t;_MP4r?c66±ĎLtV/[24;7}nO;u-"43en1®9z;CW4ae< \U*|D$04|ynmy҈xBTY;4&dnEnM$4LGOžׇ~4*v"g۫Vee1@>Ϗn4IƴInGU#!R`L^mQC4×yxV(7[ gp, 44fltF|h<4`Znnn=xVټ04oC7Sn`Dh?q&<3썮`4iE<Va>YBΝ^b4E6>}Lҹ4b5j< 4~9f1n5l4e>=уOи1rx48e6[u-(DĤwH(4br&ُWf+ ..fC&4!XGHߺw Ml@4*g6hͶlTVs7'/04t6P*Ng8G *~e 4:ĎP:A yk: 4ږF23`*fOqfG!@4VpH'<^7B4*d\՗f$F9=X ^<`Ĥ4ڃfd Ōgu؊a4U=h qu ;(bJ9`4<^b޺Rf+9 4f8hp.'@4ڊC4W۽u~~\E\4\RcrG$YUAךW, K4kZ(l9GʽK 81e)Z(K4I?!*.~q=N8NJʐ4l[( LZ@:yʠ4,ݫK&ۀǔX;:~4e6jLj&c#e_ 04j6ޔjˍe/xehHl`4kHm>+NC˯ \5{XT\4kE6ݡ;Qg|?)J4k'g؞\E%2 B8fl/q46؏[`X@"4K4ޛ|\8S2~/G(-P4*g4سN|32,h]4i6DH'u̡NP4,guoƃBHVKƞ|P4y垦.u%\tz`4ĺV=×*6RŜm^`4k|טYb[>;go"H4iNVX+YdRFN 42?dӠol4,'6;Ǡ~ 4c>lR4q+4ew:55.u;%/4XmvأZanrr5Ϭy 4Y7wyBFͩ: B1Ls4iԔз%6foXϋgS4bD(u6ģA9b7.P4(4ROurO7\4b\Nͦ=5Ҙ|TŞ 4DC6oG>ڐaD&S ( P4k+36Gt;y<4 04IL7$PlO xIr{uR 4,=Zjβ"b/j-4LHg%d 弾 A?,(Pp4}I%p?^!S;SA`4L%?-la̳ͿI4KI%7.㼪3k ŬU'V4I,=u"dsmgp4tEVfaX=3|D4b"6=pdpb`͠4Ete6 ҿuE :3' (4X ,_Zոak47E֗ NF*b4iH6^Ӣ[I0?T4b'>G;&a Om45yoOv]8$ 461b?g{K =`4,68(}p4Fh}+xX dt4v(7r:%T&àNPP4I?1eNU s ? 94<4ݣtF]}"=iO4@gփZRF Ӄڸl@4b>NSIzD4k6rPzbV"әQG4 u|7GWT;jybp4>D;=bܗ%@4i*lڽVlW]4Y1Յ04(r6kG1zЧM 4k08 g WQMI4dd7vM|Y A`4e:21Ԋ8~qD4gg'ҝ=+_0.`4d<16%14vg<*-E0"i9`4&B}-b c4.X4BYl(vXkst04f-tNCjh`4be+?TQѲP4DTT/>]I?3cqNFP4Dcu;ع'>%Hhl04fm$AߞxooDZL E4gdaDsW,KmN4<G%N]@ɜ4X嵴:GsSkdq4@dmjX4˩Xd48Dk/4KL4D'f05tR\1M04tgeD]&%$n<4,Del׵GJ-Q¬Ȅ<Ӑ4g6/a7@4,g$GO7w%&*#`4gkY1@A(PGΟJp龰4q?gk9YkIŵp4?4lG@Deh /㦫 -J4ktg<`z ;]Ei 4kg$ odU4kfgk؏KN*Z2VH,4kgWP"kpeDk~+(ѯ]04ke:'3ӂk)46-*پUڵnJѠ4@ggu^ JI/bU7a^h 4g:ܯV7h"4e:և314/uG@)d4k&5X`W 2&h,vW4kIg$@;:SQa,4kg]h4/oIF504Igdi-3hg[]4vg]pdz#{쭩Szof0P4iHU6D0Zu˱g,J4g4cq] ArL|}|ćB`4I$Ej[Fؔhaҟ34kfe4 s2JjOá(*M'L 4kP[0W2G=zQvoL4by&U:AezAH&4X NY^!tEY diText0003.txtglobalisierunglobalisierunglobalisierunglobalisierunglobalisierunglobalisierunglobalisierungnureisilabolgText0002.txtKLONE KLONE  Text0001.txtprsentiert..Text0000.txtGOLDpython-messaging-0.5.12/messaging/test/mms-data/NOWMMS.MMS0000664000175000017500000003573611707534031022717 0ustar achiangachiang=NowMMS NowMMS Test Message"application/smil.yapplication/smil"nowmms.smil "pic1.gifGIF89ae2fff̙999뱱!!!RRRsssکJJJ333kkkҡBBB)))ZZZ{{{D&$L+\\=}p 9p9! ,e2@ @@ @ l6@ @ @ @ l6A $6 a#@8@0QLXFxHplB1Ilc@g@  C p|#!@@ !$$@|A#XN ql@ @lŒ@ X7@ PG@ lJ P h< @`p|@ 6@@Bd±(@ x86Pb DE6@@21#l*@@BP#HA  F@(!$΃\,'@t#t.DJT@cRG!@$ 'X6J (K 8`S  0A"p.G H G D ( %,$@bl @ "Ϧ @ @@ aL `(>D H@"!$! J 0 ŲqO%4E J R$@ P|@ 0DBH<D1L, l(@ X2 (I!X\,K $8,6@ C$@q x |6P@d.#8@R$@! 0X6l*@ P #Hc BO"  \,K XDBH|!@:@$:@GC @$#FH s$*@PHN 0!`S HtA" @ @  % bITÑ B"@ H  @$(>D @"!$@   6  Œ'A $D@@H&J @ DB$"@ <l(H$Kt%` 0 8$L6@8@G$C  a@ !l PA P.D%(> J"%6@ DJT@&d#H@ p Fi@`J`C @<@ @X I y8DBHl*@  @  0 @&%`cA@@GX @r$. a@ a`S * A"@A 6ArG %@Db\ fs "P M%hF(>D 9 @"!$M2:6BR ec  P  J ,J Y@`@X Pt> tl(D(cH X6K x"@ sl@6@h@$( !,A 0P| P.Mp@`p@ ADTDG˄  B0T@`C ,$C @X6A #@"!#Q  #HG XH&@pl6@ PBHPX $ A"<D @ PJ`!Ib>@ 0 B!x&M%K 0P|@`"!$@ 0 6@c#H@Q|&D.@P J %@@E$EHD@`P|lx$q4 EBHѐL,b@òOl (!<cT bI HXDC 8`c* #r|>$g#8FX`!, $( !!@I$& ,Db$> l $@I$6`H$6 X  @ `l  @  @ @ @l@ @ @; "Text0000.txtCheck us out at )&audio/amr"catchy_g.amr#!AMR <J5Dn &a"٨Ya 0<B9~Փ贵`Z0<=+x8^O1X  հ %lx,Al=GQS-;P<(5Q^4a1d*Kv[q<A{2x-v'y<5Mr wŞRbP x <@kAK@úױ+צ0<:2)i=55 п@̯f؏@<TlK)5 MlW߻̈́9E4<"4An>2\XVj^=8WCɽk@<ExY<#p2, ՠVߪ9B; <Adò;4% `݆+Ĩ <4JsvlPP '=N76<$Ǡ>('oh ~V̰o_2{5vGp9f0\A |P< U#d`ꗉ+?|7#lp@&25iZ# / K?a<4d /z`~hμhdU8<$@$U,2(ւ1t,&NPnP : bxI@%z~<-!o26=u.:5]͸<@g!ܥNjU͇K,<1>!0rqDpB'6Ab9hUL`<TfA>)(jtګWz6̱Nu;0<7{#v=Hs/+0yl5QN<@g#zE3~Uy2 $ 0c?0<B,#USl+|LjLv( eep<y#ɇ~Af[ $bMPV}gV.wP*Q0z E/,2 2SġٵRXK*\I2%<"&?Y^Yv̀O]`B^0<C(O\lOhcg*(!<~ <0>tL `_zx.L <\9(՝K7}AIxR馞=@g K3 ' {]ۀ<u>h\TQAsfØnB.<4!=nh!F{ `5Lx <dc3IH.󡶶?l窕]<޺c^:5,D/i;7rk<s{6~Ap}c|lXFr-@MQ_O%kj|iJ `<2c+KtC?*Bm-!^v`<,)#*, Psm@<w4q O6Pdɒ <&ypL肖ٶ<cP <3]6^Z 'z[G1c7B1xp< 'V!hF1r P<,t6a. Y E8033/\<d:-}WAtk٩#@<zg3_aK͸z׾QoMD,mq :_ZM$ࢪ eP<u ~kD OzHL P<^0b(%?tɷFfjIoζ zfP,z=~4-* ݷ-0`-_ԆOhLըc= ŠeJd&!#Uq<ve_T1Hx)\bBa9T7a `sհ<=VfǑ17 xUNJp.)Z)<(oZ/P),B4[!E='D7U$n֚C0<by6~vL( )1JbMuoF`[@8>8lep<^v~Dz?=UkZ>U1/)9١RJ <^W4-O8Sf$:jz`<c-2-[A6NEZʿza<_=1GF0QԋrԟYP<]+&A8)V{åGP@0<^>#^3p;LH &sа<^Qރrv4R\0R0'p=ˆeQS[`#LБlo`<q!dm-Ez_0<b ߥ3RA=|B= y^` <Wvhcm҈(NcV@7yzAo 69S 62x/U _`<->"U$5B5=mrĀQ?c;qHkke1Text0001.txtwww.nowmms.com python-messaging-0.5.12/messaging/test/mms-data/BTMMS.MMS0000664000175000017500000005047711707534031022560 0ustar achiangachiang>,BT Ignite MMS!application/smil,napplication/smil"btmms.smil >"btlogo.gifGIF89aC2ƶܑqqPnpaFF:3 *))! NETSCAPE2.0!,C2 dihlp,tmxӃ "l*c( XQQlzK@:  Fn3KO,FAav4 #ruuJb9 ml$ rJY2~~ ^2{l %` c0 -q]v//˼(2%3IO*k7DTB@J"=@@F;lLX3?#y@.ځhܸX Gmڲl[z^׆)O k\r_dž9  $P!>`c~,:G 0#by3dh`<0q3`-EA#F]4SLG$FV]=T\v`)3!,=( $di% p,ϣ(I'@R `  FzrƤU@pcxQA}`bt9&f$ R@m|& "rs 4#3] K0k1}]J4"'~ @V%z\xA1 ''&%ɼ04%`͊E`J sP:甩iԸQ?&nDAl(41'~TXz0\&ҝ`H2RИŞQ"9gS^F\:Io\ XCLرd! 8ZN3h _{e@GTf9QPB-7x0ı )LP|)JK9 (502 m ӰA@! ;<`9"B݆!!,=( $di&Fk Btm$|2P ȤjTNGDz!&sԁu(lp܁/ 嚔 qr]\^ave={Sw$քt1T&a0^s~$P|pY`9ybrM˥USO5%ITeVP w/kQBpGҞ8t,f$T2P3jfy}wFEU}]X_% Pb!a6!!,<( $diBp Yg@ L+0H% (0Ш4vkO \YYVMcc`PͯBq\ wAB"qs y "H)Xxpbex83J8 "cU$ %e&DUb‚b@(8,&)B7"i+ٸ+ѿ޻cv2{'Ac1 scZsi370\"@1x d8"^-V(O$1,M_q0 0 _@i& t7<*] 8Ft Qh :T'(= E8= en| mӿCIGTq} :M>k ǶACF7+U*eYS)eN@3e\ RSeJZ|#&~2Bp2 ,ÄPŃ&P!,9) $di( kpB0B -m0s &\DʢLB `B37l 8b]tx; 4Ҡ?Xڃf] 1QcpӁ}1B!,5( $@, -)0OoA"1|$CNkJ:,Q@3Pk X@R ehLz.Q 3U50~/uZ_Ct9":ce:," e[{e|$: ,/" 9$ͭ#I"s I)X:Ԑ^M2F x 6-(PP=/8lR`,`#4 |8h5 BH`\?d2/PrdXϛB5JsŀI.u T<BD:_L4 N&OhYh7V]Q/Ro[4\⚜9qmxǽ}|02]ބPd@r9ZQH8Xq]*[S [k}5z3^0ԹÅz"o(@!,1( c, 7C? n(%qЏ #'c%t#q)c6FX0_%.Qk@Á# Hyc"ig2 1e1dBfA Z!, -( QB@l#? Bx n#|$ QT8>I\, j*( z,{zx ft$g2yk|_~~s" 2ij^}b,W I~.U.w ,nx+]_ [#ßl 5D??2=ԩ d FC۴$bu{owiSրYm1 =P؆ 8T8 g`N'+ǜfGbSA,xI`ۮe8}щIZrj('p9FթLGlj[7 gڭfeD)"ǃ,fx83AH^${j0zo9k,4͏X *nM e4!!,5) d 0HAe,ϴ0;[$s AqT"duHpπP@Ubnpq zU N~BK4 aU} #eG K 8FquëZZN7GɽU4 yJ|O <;8<[#(,@_= >y%" A> 5ᭇ;q"1ѳzxaERq:gx& Ocˡ] \IBj+5KXI'ID2 #\ZeRK̈81ndX[ 8h.=5C^ 5$ fsUI+:6"͒rrkx}~1͜*BQ.hɅU ebU(9kOBF\E~>!,;* $di0 4p8x3 Rp( Rl Ad!lC`HQ*.&Cb}o;thKMQ{%~ 7vf\z Xx:5 aFD$cD & іBՍ߆'伾q,9i.2m}ΠKp/h+q08@`_ 8뻆)p`}T7B9T!YA}#a !!,>+ $di@g뾰Όc7 RA٪h G :[BvCB JZ\W `0$ \aXr Is p uvxzfFqbQzF[?v5J/3S1 +IjµQM&΅Bӏ&t+13z #ͨm>3ڃ O;y09U-n D)i"VxıG~XRaT3M*af8pJJtC 4LC 1TKX@+_]@ J^[R !4TFX]1-Rhp {{?z3<#>qAd L\BrfxmNuEj[̽ 16ԞN($V >Ld| P 2x"( 4u&>e2@V/k@}e)_5-, * $di'QDtm40F,ro ``VvUx0>Zmn 01 Њ~WH.qr?u(wy|{iYn6ilL O'z~HE+p_""{F+P*_&z6k,P"*`(·*&ug ''ͨS9#&&(x)   x`q P4 AGE>$J Xl6Rr'8?ito $T(/ 152A@HF t%ɦ/-sԺGĊ[#?SAЍYC/a&9= 03("FcΨQˉ (h=a$!{\fB UaP2 LӞ}x)hig 48@nFݴ9Gw7,iQ' \( XL@ ͚pQ8RּA%(0%2@s<0P귥$8^Q KkYX;w=}ÀFu%"\ X|oP RݫpZ:J.IY9`U*G

Q7A_F/<RA80aPAmMHIpL3I V; )&audio/amr"catchy_g.amr#!AMR <J5Dn &a"٨Ya 0<B9~Փ贵`Z0<=+x8^O1X  հ %lx,Al=GQS-;P<(5Q^4a1d*Kv[q<A{2x-v'y<5Mr wŞRbP x <@kAK@úױ+צ0<:2)i=55 п@̯f؏@<TlK)5 MlW߻̈́9E4<"4An>2\XVj^=8WCɽk@<ExY<#p2, ՠVߪ9B; <Adò;4% `݆+Ĩ <4JsvlPP '=N76<$Ǡ>('oh ~V̰o_2{5vGp9f0\A |P< U#d`ꗉ+?|7#lp@&25iZ# / K?a<4d /z`~hμhdU8<$@$U,2(ւ1t,&NPnP : bxI@%z~<-!o26=u.:5]͸<@g!ܥNjU͇K,<1>!0rqDpB'6Ab9hUL`<TfA>)(jtګWz6̱Nu;0<7{#v=Hs/+0yl5QN<@g#zE3~Uy2 $ 0c?0<B,#USl+|LjLv( eep<y#ɇ~Af[ $bMPV}gV.wP*Q0z E/,2 2SġٵRXK*\I2%<"&?Y^Yv̀O]`B^0<C(O\lOhcg*(!<~ <0>tL `_zx.L <\9(՝K7}AIxR馞=@g K3 ' {]ۀ<u>h\TQAsfØnB.<4!=nh!F{ `5Lx <dc3IH.󡶶?l窕]<޺c^:5,D/i;7rk<s{6~Ap}c|lXFr-@MQ_O%kj|iJ `<2c+KtC?*Bm-!^v`<,)#*, Psm@<w4q O6Pdɒ <&ypL肖ٶ<cP <3]6^Z 'z[G1c7B1xp< 'V!hF1r P<,t6a. Y E8033/\<d:-}WAtk٩#@<zg3_aK͸z׾QoMD,mq :_ZM$ࢪ eP<u ~kD OzHL P<^0b(%?tɷFfjIoζ zfP,z=~4-* ݷ-0`-_ԆOhLըc= ŠeJd&!#Uq<ve_T1Hx)\bBa9T7a `sհ<=VfǑ17 xUNJp.)Z)<(oZ/P),B4[!E='D7U$n֚C0<by6~vL( )1JbMuoF`[@8>8lep<^v~Dz?=UkZ>U1/)9١RJ <^W4-O8Sf$:jz`<c-2-[A6NEZʿza<_=1GF0QԋrԟYP<]+&A8)V{åGP@0<^>#^3p;LH &sа<^Qރrv4R\0R0'p=ˆeQS[`#LБlo`<q!dm-Ez_0<b ߥ3RA=|B= y^` <Wvhcm҈(NcV@7yzAo 69S 62x/U _`<->"U$5B5=mrĀQ?c;qHkke1btmms.txtBT Ignite MMS Servicespython-messaging-0.5.12/messaging/test/mms-data/TOMSLOT.MMS0000664000175000017500000012504111707534031023025 0ustar achiangachiang>O allan@tomslot.comTom Slot Band#application/smil0application/smil"tomslot.smil  "img00.jpgJFIFHHC    $.' ",#(7),01444'9=82<.342C  2!!22222222222222222222222222222222222222222222222222PP"? !1AQ"2a#Bqb4$DRr.!1AQq#a23" ?~TnS.Éa0sOSվ8ii׶{kʇC:βo74p A'pvn/jÞ )~8ScLG\* A7+*B>\:ꇸ9tN"&B1&Fsyd+!hWB:UUmոY:fao:$||j @YQeOmj7,8ıۍғ\FDR-mB7ͬ}ǷlaKlO([@% :29a%n 8h.&,u  NJ dG9Jn!MV\ba Pr!<Ag^lN0ܣlo bu4@=q%ߦ{xp*(ZB[!)?<1\nzm\pw)mhPQղ@ ŵkWp랶U>2%qQ4|A駘 9K Aj I#,Kǒ;tOwW/VbKuu9 6߹V,Q򈽠Sj뚨]n͆ЧU̬:{SfH<؛s6RVu$:T;w n[z( q>ЕQ%XZtgq7%+~l?cWδV1˼u2" LfHœ_+~ZOz<ۣx.Psܼ9}jng ݕXYi?y%z'8<+&:7W * lR[sԅcdLN2)vau#Բ2s*~_ؑex c]TĹΦ\--n[ ?]0ܭh.XDjq*9%KYRTu.!6-Sus4oM1zju4je`/mi7lV4%AvRYDl(Z$4ITW*!)e.a8s;'flcnJ|4g8W/bF{.bm Hu~g9_ۇGSy7J̪w go:lxK8E9q#̡ h Qo --)ij+BԺyydz$c;jp3l]eKv-EFP)HQV@lh.1)v+/qe9+JmIBA V3=Ev5"~M7S *=zN߶vʿK夶U QU\o H8n+sKj uIGN>0fiV9J_J$۟g56ħ:3A;Hz¬ĚxtHΏԡRƊ+KLuw&XKHI Nݜ9]ika |0JJO)6t{ ea9\)W |3MTͮǷ9Ue$s$j2+V,Dک_ 6S.LtIZa1@GLO#`f$47ci''ṠONB3Hin|A)vhp;?)nIuN%Հ;{Hyd~7]`ԇH2wFЈ?mgei}4$LFom:~%,y$'I[7D?Wf9G:(< Wi U>vKEJogc k>/=/Uն_;bk{89<;>'INw-@ l HPzKX'k^UDYz,cN!0J+ ':ȿ}o?ĎK K'$cHǺAW#'myСAARCZ!7*D> 9: m)HޒڣFa'u$圝"47r]$xzWimg01.jpgJFIFHHC    $.' ",#(7),01444'9=82<.342C  2!!22222222222222222222222222222222222222222222222222PP"D !1AQaq"B#2rCR3Tbct+!1AQ"#2aq ?*{b)5G ΄BAۘ;/ ڵz1yΫ~z鍽66AQܙ}ɖrF9#ȏZ`I5E^`-d <뢱S˴ '=b\?s]?:s=L8lBִyO;&\^Vا{ ʄ$ Ϊ_|U!P_¡{2 PyFHL^/&Tv=48iП|yivߡQD$ \fݭTHcxP8Z֔J9YRm{qI)ߑmΏmдǶR̺-ȲEHT@ BFHn4&a.JnlReFK%8ҕ흳un*3mk#m:Wq U#qVSiQ`\t0[?21uϗ*Me H>%Z< V+~o@K''F\rKڜ!g0(4<^*U]J,HK]YdWEopLd5891]y[έ(ij8 dǣ|u2׭iχ-O62 AMrW8DCR q Rp0>.zI0%L+RִRz!,bf #8^ڿPMLTJ UJBp04l(ً$\i)?aM,Ì1t\j\} W?)'>9cZmx? ӦPאcV;gWfJ7kESXG[e5g7>bWl NDr<0 #(ii-2m# BA,[¼4Ɛx( yJH'Tj#:KM=q#OHZ)i4y)ɝ7ҳ,|5HNe;-S[y +KRb> qLZ)q~# Q{2ZrZ[?"'7NBщmL.|e1%x4+ KzZ꬚Dʐ]maMU=qd)pw$$gc8B::=g{\.EҫNB ꎒL!A*].ˇ~ǎ$Hnd BO1:15:" "Dl5'Z{49i[:UȎ!T,%M2,\/#~X^FOR67g~Q,[mF1fp:C*'A?v A HI?pKFvM.|H堨2~f{ ;l=tȋAgǏMm>NG`nߌs#Z;ԪeYmcτT7rF÷&Mb{9NIh'6P|_-jDPP -)PG(|RGK(UGmjrWv@[, o l!ӿ]Pe*vedg*TWIJA ەDFhh:maԂ~i7Yqwgj?窒|LI7Me?i4{+c#ٶ+8ʔOcAq#GZqn+~TjƬ(]$%yt#Ig?I2KcY6ҙƷ) [vmXug=NMH0"Ca#-6yG@Hꍴ5ék/ܖ6?R"lLg*q(4x*\$r*8{>5&,)4xi$/!n+γ-:CM̋ʥ!FAnDvaLv>TdN &:ʅ&MAL*\&bGN'>d'*P#(_$9&UG4E: Ea@wT@'q>̮%N.cN:^A^qY{Č:Ϗe?}6iB2Ppwe>/I~Re S%"V\VݖB6J#]t r4[ySn$)*Db4C39%RJrH IOqnqfk:%~GB@тQҸ=unWKTd)BşMTKv(^748|BF{swկOUK)fW Uk~{3}UnVDPI@e֐IN>nQ0a`b- am%JQ?Ih7+4 #.M2a*[hW \nK%եEǎvfG0I_P"img02.jpgJFIFHHC    $.' ",#(7),01444'9=82<.342C  2!!22222222222222222222222222222222222222222222222222PP"< !1AQa"2q3B#%RrѢ1!12AQa"$q#34R ?~$ԝW]Fy2:qs%ƺFTR $p-XHPB@*ǟ?mJ{I3[~zp?:RT֣X/sBLFQ^eNh}%K@RyiwDK&=^T`0`:C ۸eJ89?N;vT%Tvr^|=FnUVҜCl)Vs9zt:鐓Dli]-i*QQq)y %]圍'VZmrӐ@XJ@ch {Yv$NٳBK,XHP_8kfdx\wLp4+PIm9{jrP'V\L͇@#v9k?:[όpuƚGEm{_RkQeIۻJQ@? I_MTtdR'3*ٵ^`=dp Er/,<ˉR=}@$ϗ'bMnH9t#* 3SY7( 9fSF?PY  -xץqJ)MN(%Q!#4JOH S疒m~sgKj*zWˠkN>OmTOJ\OP:~u.z$&(*B6h d b-ڨSFa,(tcU\RT*4I(}'c?q8jh=cP6Zt&vի(GQjAq#x$wf]qo y|!Hg~+**1ooo: !R"q(+~^C\yԽByhj vU6I%iR})Txnۅ=[?LG ѭ蛅%{p՜;V@MYri)-ҫ%J?a-b_ ui4ο[4˳Rm/a^1tUNB*Z+Rd@ w|0 qKOl65BRjE*M 22R{}ʴ-֠3Ok! ,9?-Vӣ׻HFO0PITeD`w`t՘+S.+—WǼja&$c+!f [U*NHR˰[+Z[ps1EJPyѻ@P?-_jrRK)K()CH$ dkFȣ*}L *N[O4<)3Z Hz. `cv,tH]u{f< =U[ L | ZH)P#hͣK Ouɚnp6G/Mg*C1)zf.5U+a%#ymdCQ@VDcMkHJR'r-[i'ă6'>m٤5 a#;i?Y]n'MyP-ti`(aHI>2r<9xk]3ccO-9uoFn)wC$T:!5UҾɦ7dz-tsάڅQ^va(v2x듭*ÖmfKKm0a!8NOaHҕzq*N֣MyӜ)Y 1uي^PO?Tʜ DNc(4QdIlLJ*S>Pm>9:|:Wϫ3IHޛ4C')J%RVUܢ /O g8p+y pq5@MyRyL$xOV%Gvq"ʭSK^zJ K,#ϦC 9UTJˇQ;s̜r5C nt Z >"img03.jpgJFIFHHC    $.' ",#(7),01444'9=82<.342C  2!!22222222222222222222222222222222222222222222222222PP"?!1Aa"Qq$2BR3#rbcds,!1AQ"3a2q ?if+Rۙujx@ZKRGH8_ Ʒ\g!֕N`CR}M,ej5GsyR3'_m\mV>b#ѭ q{n&F;.N*ǵAUK8NT* mCƣ%DFQʰN:g *2LW,qDž A *+EpIn nL< t<-==pO!uD-RRj-Gg3}#!<'Ca 3-GPj-,?HɑV7Ușj\U9ym#!Bژ :G>ę-j,(RWKx.cﻸ<#dh.6j^q)꫿#U!VbAmMnC)@qi+g{1UX}b9f…M)HmFCt H*'tJU=5 uIUSU-vIYkTz]*̃MVSVH%)JRϩQ TV*-TeSj[OHP`:ʼק'wtq*F|+Qc)I8.Cx; uNvدHoҫwK61GUyt{|ݵZ;| 9p+IGlvTw85KHAVU_nZv,UPH=Sz׭K}uGa4 AtTdH su'%ͩ]YZyg; uV ٟ8%\~jȸQ\YqqXRrI93u/v JPzPFGE<3p8,6CЪURcL2= ۜt|$kр!YT?yP\#~F%]9>kePaS!XPqo#D j1f8uJLv۱ZrLj6r+n8Y[(!$J'K*%;FSVV#*88& 6nxADi SuͫNbf[eN$d({8Uie/K/s D~#[#o*)m?N%'HZ1z^s2J{X[?UIxm9f+:י3p;K-~շJw S**W‚%Q<A'>YNsE Y8 aH:֨lVS]yŶ|l) ]Ҫ*Dh]UHd'CSyk*tZvUgqlNKmP{J`6ڸq jd(SpGY*wӢ0ʼnZih\SjQ!9卣۬]gPY*+q֎Mbx_HEPwoS0Z=Mҁ"+Ж0=@p ddq"4ii*R-bimg04.jpgJFIFHHC    $.' ",#(7),01444'9=82<.342C  2!!22222222222222222222222222222222222222222222222222PP"?!1AQ"#2aqV$3BCrc5b-!1AQaq" ?'y28̕ )M r7bjtTj) EóQZk$n֓KVfEUo>h]~bj5:[-%j,t,[ Bi% D'˥ V2'=MYȟ%zP̑2 QbAl`L*&ko0mGflBz ?m4z+=H߄՟̉_~V2'Vm$$.gԨjB 6emmåw-ZvdVE~e)}FxJpAq/3Ay›KTDeḽgaJBy8ȪJfqi6ZSnˮӔ8D[ޏHzG)4"={2}%ƊIs|oKaJ_2NZPLw"Bor^ƧhPJmΞk4ԙeL-HaP6#l_\",50 }fyf}MUyKYRU: &HJUٽH2;XRJ*Y \2 O/zmN?ϧM~,)4B+^.Ch6X?ė =(U6CʎSה$kn_O6AhuPXRqR-g&rM2UR#8| K2Uz1i**,&[϶k b.Dn~iVUJbT w*{~{ WXXrH91VێsC:I2CYSܷUǻ}p>[X 1lmOR [Uezr“ptj:z_01C{e.`16jU>!ѷ=jl+RPm8HZܚiRR6|q/( uE] $mD |bGNmRARYR" 'n!:NH%Z qWjM*dOFX$>VP'HMErNx(zJ^$0[+ !obm6H6W+՜ZSSLQH!!"R@[&Y\$QoHmt 7)|aD*(\T Pa+Co"oGn؆h\coRfTQ/W&-JԹ[tzIv=a{_2<^[b%WcQȝbȾU򇓸}A.;1ʊ wZK̀;o;3WsIRlE6ݚBj0\/Uժ2dDmORE'dɮf6ܐ RBt{wTIyn):W6`" 2'4Bu&;j)@|UH$4l qwƶ[)wRbw+SPK3aeoKMQu=^bFz%}W/Y ʖPCζ96r7STs٪$%Əu2<}i#֨0N~"2>I:4fiP s}uZR?b۬4 [5 Bf6eH)~mҙdt 3"}j)ɮd5RPumX 1kە5%Bmp؝ iYK3_)̵K$)*M5$qT(9B|túIױSklS GB)wQ[*HzMMe’i"~j@Z~85BB>C⸍LK}s-<䤏e ->bģy<XGN:Ȓ}(4ŘT*m m_,uL~{a51pT-nܛ|0kQnJю xڀs!a[}͵bGO2_j+QC GZVrSӸˢ2{)˷O#fjrE͇{9j #"H/'68&;uzkEvEe-Mŷ\#U]jG2hZTZ~+kN0w޼V|*Vg Nf\qOZIu6 R.!4孻XSa"txt04.txtPresented by NowMMS #Vaudio/amr"aud04.amr#!AMR 46=ØKi4&}Rl7l%{64exݶjlV` FoN#4!b=?-[& 4I+5]K_,>$6ӚNB݂R4*P@!-ڢzA 4q5\[ټxP4*%LT9C,4R $Eo 4q$u/D:WBg]m*I4:'?n ƖIfE 4$^Xũ{ UMp=Z4*<μ>M@չ-:{ts4gN^9i8֫%I}$p|o4xXʭ3sQ &']T4Ϟ-oR/'xpַ /c4צkDEaqg+y;`4ngTgis2™_F`|n@4!=7 T6S, oT4*ԗqz6]WX 4$4D }w{Toy4ˌRf9ŎQ%ీ4NLޯJQ"/C 1 $/34q1~tej\А;KT%4xҶܚ8]ᓕ2r4}X&d+o4w@4% Z}7B# @4*Ƕ{U';B dǍ{ `4*@_w7lIht2i@`47 Fr5NId#D 4*64ъ@e4KۺA&5`4OD_^cKAYwp43696$[X{M\Z4*(?6zM%ZG`p04όQv?՞V Yg_4*dFq+ 3HTLp4*;vc,.o!r4q9?7Kcgt}n(O4q(QD`yGgI`<1Ka4@-BX<Wn2yr?FG4ћfdkiHz4/ҥT>x0Xv6L4~7Egg q]`4DO9%kPĶ]R`-k]4g eٕQbIeGQ4O2p*`ʩ֌ 4qpwW*+w4 6c*}{X.@4>?v;0BUuod4?6:i޴7 p4FrE#gp-cbVJ4aa*@,f%I4vA7E,d7d=4!ew ?wWWu\xL)p4qC6Z] M*z!%6JP4r'Vvڢo5}^"4q!~z6oo  P&p43)ΉG{\4LQ6؋*QP#$P*':4K6JX>>/%d}p4dO=63~#SUc4OD>ܭj z<IsRfQ6/4!O=u"M?œhgaC4Ϝ6\?M`:bA;ȅ4xDU C/0Io\ 4q!L׶}<;\GF(P4շ۠/& 6uH6(Sգ-4q=>ݫE֓ 2aBp4G1]?I4˫EfGUT1JI f8r$ %4qm7iѸo.d`TP4vۑ]ts1zYWԭ04-|܁;ѨG:zA p47piFWS/146+>~r4:H𕎎4fK, w@$ p4>ˍ(Eh34]5 g߉1=e43owi&dN(.n42/ۀ$>6‰Nd4qr%~<쵫p?\nu;rv4Ew`2sIK;"`'9pCG@4﨡@*P'44q"#9HqMѫ4*?4Wj?_g+ 4ڹ@kMr syf/`yV 4˚?t/+s:i/TP4<.;9;y)NTQP4ᄒl1b(#Hr Oכ54?&Iu~8J WX R4*m9amأAI447u*A! z׾j~&4!Y|??>FvB^P4-6}O쯳-uDt9%|04<w߃=6;F{Yop4*6p*f'F$(|c4qe6ګc "x98+?Ik4 ѹ6?{>:*ꕘEp4o\0 gSg\7і4pռܢeL|mx" 4qHdMEB 4*6$d]dţh4*=WBf> ƅTi4*o. 6yt2'B 4=^^_rF&ma![ |4BoD^L9~$P4ME4rԓI4xH4fO[}Ph* OW`4˼Q;~]eg!_=MEX&f4dҼղ ?ቺH /P4*(~־6R<q̫&o4|&t@mkzKP4xf566ķ"x6Y`fsG@4*3=vDFv™4geE\?"l1hP4.'،a}MꡙX04DM`/C S!X4˄1?0`<~kWyEłGi3 W42R1TJ"w@Jn")4G6?WŎǙU&Dv0q4 NV'{U5O@4_Ъ6ܯ sG{)429ׁx Qm04BMDx\tqWNۉ4*='6, b`4!Pп٨<\=FAo]KA@4L E6Jڨ%d i!Bkd`4K@~9B|`YǰÖ04q3C69WdF\IPMl04*3=l߬+H~ JC|*y49ݝ9)o7y2σ4q3Aa7# ?`004qQ[j  pT.\ 4*9,`ejf5|NG04qJKi v.& 4E4'C֬+pMo@4L-6ބu-HR6#}?Xn`4AޞEljIĭ 4q2QCvk-|LW'kBP4C7dIT]z44eVe)u+W}>ɣ $W4q IGM|1DI[W ",`4ݜ6w<|RxC04e۶jQXԗ\7쫥o\*4t$v[xa.T4q3>@Xb2aj'@>4 3&ܼ硩hq04n6T|v<&Ns6}@%4g$6ZDi. aP4k>ڋ9x"(i2ǶwRޅm:+>!5=Tp4*sB679 ^ą1ǖ;j/ $4a;7k96<4:]A@SK O?4qO;69PS%؅JXFP4?շ޺J1o=)/̤4~0U$4EeE6+ѯ=!Wyxѭ4Τ&U>,^4䔨z]4qe7jUOY{S6kM4*Q'6q<;h?'˰4qمq-&XGwP4*QLؕ3zX:$+p4~ֿpE14L#ea1P$4F?wy E ]2ې4*Dߴvr84*7>qN%D]/>B)C4τ91;( 79 cy24㿜59Saݍ הc443OfN0bZ>4*E?n.IfOw7Y!Ȗ04*t7]BFc%etAt4L)of~,^T]1F4h4#\kWCyz0r+ !@4NnDjO֪VcJw`48vĹRכϜi1c04rOD<ʙ>7hs%;'043O9fL?3hg@*,I0|t4U8RzGX8;G [Vp4ODU9#@:40}FLDS4fGWN^铑{ɿ1 4qQ!؟Dz]НsS~َWO4q!K@TΝ0;қΑ3eDNcp4<3u5M,̷N@4*=>%X홴<"1q+)u4rLQE6#[nv"iV7q θp4LQE6؆ 6x@= YoNEP4ar>k6cu~*US.p4q}E6!<*@={Lk#`4nV,;? W"zh4<'*6ߎ9Dǩ7cjO %vp4q3贿/,UIBs4%4,_>X6l@4\7v'zIo:04qA;6 xn3*u4*C=d?7ߏ0mHS304~6zVTG{(4*ϼ?ވT-+Y8Σ>+4@ȶ`Wč5tLLh4BC6&w"f)-޶4D6Tg~\ؽ/} $Z04ܼ$v臔j~ !4v.y>T"pmR򒑒A4񫤯<|Tcm{H^U3p4熥>3ѵJ?}宐4*Lc;L->?E043ɶ糀0*#,މbvh4*!kvH&'Td5W 4Ds6݅?֧jCnE@uƯ|@4*6@6~8te;J 4*#%F59+!6WQnp4𻿜9ϳPeTi$g0k4񐤵螶[]>}/4R6kCbҊ"R24lm|BTiXΏ$wˀ2ͩ=54$'49Ɂf狆Iz/I4q6ƹbjyA$%4q?^ 'g{穼X4 fUu,A{`aiaX 4̶}t%b$wS4xJ7e٪'̯Of[Ri@4 v*4jyXCA^B46ٶ܏f+DG5, [y4D<\iR"F-84PҞUl (_4*;>ai `Sy-DeHJ 4<6t^)T$]0ıuP4DڼSgv7pXct 4DҾ՛lzPpIf?4@ 0oŮ/cp4=7ن$4ӪQ\@4=:ޫћ .ۢ , %܀4*37ߦzp"㫾Gl 4QY]p˄'?Q[(p4q)٭n)P`4˛qOY% vFp4oǷxl,Ψ@4!2?Ӂ\\]c^o`X4QDظD y X8@OP4!QBw4Ϣv۶sQk]X4*񕪄7#uth4!,7ܽup݋*? 4ܤqX,SZ34*?z*CLB̠ 4~>K[\WK2T`4./9Rqٲǵզ4h&)OT=D4'g;8nР4"6Fq/|`n 4%ؔU'`et/0*4c7愚VːDv4aB~ Cc~W4f/~7d']І&E4rf6?-*T94vyFt%૝_O4عڊ&!R p4@&Q:hMo}p4(;ٿ9\ (ǩ]*4@k7@-,fXn# 2)?W043k69G#f#7n=rPp4qbuFl p [bwi4qg-LV&.F\ސ4qNG|g;h47 OdN̓͜oeP4rS=BU{a( [yP4<*|ݶݏqYu' %)@4pW޹|53E_'oU4K3hXܬnO@`T6+ڭ"a4qPHJlܽ+v;vK)P4-6w%krNnj H/)48ۼ6jNmwU! `4qrh'2TnO<ۡ]4tg.4q3<ڇj` ].@^g4h>ң;lWʩ: 4O=6۱Do[^~uL~314ϖ˦6ڤ8U5vK(R4*Ͼwj:tyze 4*OIMBiD]4l[!AtYD4ot֒`t4*YE)ŗ49:I,~$>9 4 xCL紖&049WL.Ť4Q35c+n%}XlBdE4۹>`ׄ Yc 4?*L, l@( !B4i4ԕJYud p4rDS|w!O^{Vv붸e:4!hٖ?ym2ЬBi;[4t7 |7-X74xfi?}6Gz⮻A# @4&ҔE/4 >QNȌ@A54v( 8Qs,T ]#@4 3c % K䑄4! ݼ͙4 BXSH@4ͤӛ4HMQD?hC`436_?df p4v\HD󇀆%B4!=z_*Z ?R4˄6 TkFwTw8Hͨ 4r7  6fp&4Z_4Ҷ޹'#ى("{ǘ 3.`4*!+g%Ue8 4˿&e\z+/4I1&9b2mod}X`4r"sU_d9s 4r7شqݐGJ `DAVD~p4rԶG'^O4r29QJg[K(F:@4iAev%Xv}a6F>4X*>jWեh;|KE 4q >GN}9S ^^4D(6z eɗ!4oԅdQ\󶂘Txq?A4=\^bKtn2*Ow 4k֪ݜw w9sm@4) vezvL4(-'oeSώ04n!(ڝ^򩄨8fiV04pgK3GEn%Q4 0mEN.hy4s*^$SLp!ao6U%4"7 Tȣ#@T\443z)t1)`4ueE Ȁq+İj㰛54/jM)cn,MU @4DW9_7!Ke`P4}V9G7^0Ak>D4ߵ]97mZ @ Y^ y~`4摆lI= *5΍End04jY6-ʤEļ4y9aH0( !4S#?lWɖ4[PqHvD4 6>ť=dawg4C5^-A}Уk@4Dev9N." #HIԀ4cWYw&eٛg`@04xj@vwC5\KE4bpݓb@M#eKT_'Y 4*e^FRz$-_|@4*l; N\䭶@q04*ek<$95hEr޹4ːtԎI~RsH/ 4ˊe]3%/,oC5]b4kemqZk0 - 4xgm_= jw`p4ZCcm][ rJ xH6,l4ڪ_v39]'P_BA 4u=d?>QqGp4BNdqQJv*jj8,9`4lQ2Pǻ4 4TJjEd8jP4`}ӉL])d&L'4Ne'^I?'D匵rE<4BeMeoXáX4xwi󧹊3KHNYF<4<lMM7WY2k" _7ܛ@4B*;7]O<5lG˴E46[']:{L]4 AB7t` 1qFҤT4BkMO7jEY4*K}]cMp\IXm ! 7g4S!<߼\lH j4 1/zẊw3 袹Rp4u%?} O$z/s4 M5$ʹR()ȥ,u} 4BDXb?X~-r 4 OAVv ~ܮu2`4=ժK%\0p254>4e\цǟObF˛Pӊ40l=c#p8&x-~d`4^l)|7E844: {.<448g-m7h& 4JNȶIm>d\4l8,3}bg*w]p4a*r~Wfk?2Czp4h6[QVQ6X̓s@4,LكDl9Q~ԫ|< ހ4;E6މ[OPo1]#Hr`4qa86`hΧ2j Th4n(Q=Y 4y`4䀾w7$6R Nu]P4cCQ"~2tP4RXԼmVڳ34kCu~B Y,/"4,E~X]?LoE_I3D048EwFDEq梓R`4IM|#?}%d;+`48EuXnZ&,?ϖ04,Ew'`i*iMP4vE?%_z  |c4T4En+5 zS,%4vEws` cJGx348M|.dp* 4M9>I\kPP4q>?QyGYp14xM6 hy|Y̅ 4* W ? mj"2-I04afTXa&Tr=O2̉R04KߜUR_*GKc:8&p4vAO*HVT+4JM? G c<͂$.K4qyMn~zzDl4IM>-twӓrh4,XEgٯ<lVLEE`4DƓoaV 0!4,k=~طZ!}IWp43m?FZW3c+`4< 5??Yy0a~c61:n74i?$*Ykž@4ڐV`;KDc|Ҁ4(İn `D4*2\4OiulG 4g-1-I, 2ErLG@42څ?%.acC2}sm 4 .Q?<)nU} #4Bg+B͍M7>CU̾4 ;z[Ζ-99/N7q4/5\b)ϙt߇D44!4@.ȩ&ap9-4g }IzdqUn_~Iˠ4-H͸k;ހ4n=}-h6madb8]P4w*/TeJ4Pj `4p=n9a6 ɐ_Ic04pE=K[V:=v +OP4g;_z]p`}T6 p4Z=, 6as*jVIݶcc 4BV:j=/133844P4glA++>4A^E0-]%p4Nwyiq2 /ښ4xS?sEY+,$TG4xQC<ޥ s {{||ȓ 0Z4QCa42sph;BQp4xN<՞@'PhsF˄p`4>45L^5HD4BQT +ӫ'@8 3L04B>ѯ uwcRnU"@4Bf)B*Uױ: fl[ܒ04SC?eH)Z Pٖ `P4kdK$\AT2x<cSW4U*ROodȴy!@4;ΐx+,([;Q4@{X8psx_4IN4T-O 8"Fc4.V==~>k$$w 4)09{6ޫf9Ę@k`4I&07٨O j{Ee M 4&*e^g?58_i4,6Leeow鶫Dŧ'4?B5,%> ȺjKEEǬp4q3|\4?gA: zL̝4RJ7_eqɑ$C-~-4p/wj/ x`V:W=uR*4$/?hK\ ѿtqB`45oQ cpڍ4b 5/(8LRc4 5v?F-T`4 5~Htșq.!{4b3-nWb=5 i04b 5<ܭNwg[#I rങb4 $=f%/#|j+ `4曎=ډ@as"(ek2w>4U7؇frj1Hd`4< m7ؔ*1nF4qg?~L5E4kQA^"4̋_y4BA{{¹?`5ŊizᚙP4 c6{̿[ؽ0Ò ;@4F-X΢@etWظeFɀ4rYʼn6j,}Hi4W:}R C?v 4]Qhtj!ED4d?ӹ{z@E4|ֻ'w5GnD4k|JВQ=8n``4e;'Gm _РY6"^;\,4]mw7i2fP+߰4pe}v KHXWVtw t4ZelMN:gu 4tf 4º@&k4sFP4Suv p_7٘h\e] 4TmwN Q X$(q4g.[>fߩa"׵4p\緖, $2QLGYA4p]<Ԋr&^e`4Z]=.^"Iy,p*~;8BdBp4e,pܠɂ@<4Bekw| &s4m5tQOOV]ZS;#TsF04BAlp@3[7wg@4c{n/qˢ"SaO4g\i߄ێR2bߑR.Q4Bk]=x]ںOA&-04&e`K[:A$Yʪq;04klfC6UCG3P4BldxDžt~hՠ804 SAd¼r3oP6&A{04 QJvZ/v=̠@4TK?=t*RBIik@Az۰4 M-G$-%r`4T1*/K,iߊu.Ogq9413t˚&[JHdRNI4u&DԞ\%1:b|-SDR4C*,{ gl/wE45,,7_nM\3 04:g,eDrp4uJhD8_\:4EjK~: Ԋ) p4u=]vd|G&Z&{D="4Q%˸AH+(Aҧ]@4ZBpMPXP4!r~sO7t!RZ=+1o34!GD6H`?+^76"]˴4qW6O!4LÖլ83@4BzM^D$͕-{- 4 gD55ű3z P4ki6\zQ*Dp4xbK4?/nJ̱U&+4Xj-Bf n?͟4x}NرXv4BL4awc?7c׺\4qP&]M@I]:4!"*݃*uu+{>Ҥיo4)E'O=<;ߵMyP4HMD|n9!9.n4*QB^^jˀ4O<E}n8d6JD5hb4ISC_M#qe/ISHD)@4)HIfl`g%=4IBQ /GmTXXEp4+~hSnjP:`4gۉAҟnj}P`4*Ld\9(pu304x\H#& <||304!j\ ˲Z[-# Eow4c|KSE˼(DfpI4e\ Ky:9-Xf)4ek.|=4.R7Qm4_}b]aŃP{2Js4xb-ˢ4Rb\!dp4xUvzS|-zGk44g5ߟwl8 |,|3@4˄Bb6fԙ:^::`4E}jA †g.k4xM.?x: ܴU ȟ9@4vE}ƹ'"09n_i\c؃@4xCe <67i$GD 4xTO? `BBV ` B4xMmow[2cS!t'-u4xMg}fIgj*\404*1M}]] +PtՓ@4 fM{  ̭)p4˺MkBY 2Zq)J4BKoHY?XO؉E4TZUWSlES0p4b]}\ឬK5.04BݜnI%w7 װ a4pcݱO"evNj7yvx4T >܈x/ak,E/$04?݅8 +b^ p4,ʿ.HŤ_G߀4i'&#h9ADoP4G.6ܑ%Ǐi8ٻGWfP454ܺh}P@/y4:O=w*n7s'-hYhp4xQ;g^;1*9V&!\\\4QC&GE@xO4OMoo`q$lכ @4vn/<@xGbmO4Pm?r\j\4 {M4Lr?`:yV҅dD4o` 4to|'`d@zn64vUf=k2|e4ډ[mUqbJ]m^I&54]|WVR 06Tp4Z]o"=3! j}' ^P4غ]oW"$Ite\i <4yeo-Ր" ˟c,[ͫ 4\eoJcYѽWFHt04X\e~uRqZ@92g 4\l[nQT )`L׼?e@4\ev X)G'ky?IG`4ڤ]gR. L= Јj4<\[+vWs⾆#HN:E5{{P4\e|CgՅYI?nEV|4B=^ՓJ߈=A4j c>`V-㹤6,4m DzO4B[5@&?{Sʡ<X4-7J–Ӛ;E=4;v94Pf]C/L!@0t4xkOB<ܬx0*2VQR|4BK6 JvmI?w{)@4TLf)*#2f31s8`4B4dߋOF#;L'n5zll4qgO4ة u*(7NG)4Bhd܋Qo\Aܟ5XaiM4O޺wfɊc͟t(& 4O=6 =\g73C #4;wpSy@d$rр4Q;'@9kSXkAr=P4*OT6 ^:ra 4i9N˩dđ!44D,Tw/l 4.4I`s04Bg'6 ^ww @47¶Ѽ0d͠qמ#nw 4<4.k?,uAz%p(P4ig4ԯU2j-a.ϐ4kk0JA4GvRхx^:RϠ4qvDo+P׊FXp4IkZ ǗͼbkWJBIe4v$E?"QD lz 5E4i5>6˼˙NP42|W"!̃I4k ,:O̙y /`P4,A%gOgy[x,j4g%> Z*{pNL,Yh4J|DBvy! L .2/e4%g ,$X7N@KQ\s 4 $ڳeBat9=(@4B_Yo S|㫳!04J ?af2%ItiP4r.6.} &JbA$GG(p4BEuy9?R-~'yF;߬4ԶeLU|%4*c6;*OV讳sSVy4B/㖶hM 2ⷌnp4t;7SFq33GI 4f4L4fՒ =ˣW%vז"4T] IbD;4T]n[s5@"VɓW4/ej ^Oa#Q54tnK= Vi&"e4Tu<х&,R>2=5`4rݼ<-^}Jꬴ!4l^qP"#H &ߎ4ldۅ #Q҈]}@4Gck2WAZٸV1[T4jq"tbW%4*U?g.9\ [u͑P<04X?]ȶ8S~^nj<6@4qUކ]R τkiB 9i3dp4vpDw/?>ʼK)14 y#BoPsvwu4f߄DՆ6wMCv`I`4o$4V7 f8rȺFC4<|_p<; IP4<>pT/xwz1{H;P4Еd۝gK(7D4x=*-o?#w_( ܖ4e&g<(G." 4ʚ ÀA5h>`4MfqufcR4*NuQƠHTQ4Bx4jTs`4A뻄֨Cc] !UT8&P4xpi?ܑ?OTY|fQqҀq">uS4onc&&rl\ lCp4*l Egǽ`[b4*U7٥9+ֿd, -n,n4pdۏ-dnK4˨k@4x?ey?QR͡)!04xEѳb3cT樁"M4C6Lwlvƒ8S^܅m4x6!1fM}/! j@46?h(lӬp4x\mD#M_mN~+04B5YvK~l> 4ⵎg0)} @p={gP4d>%/$]K;($4QL?د4aL/>$xC쪎rP4*tQ7Pס1Ǝ% -B 41fPm2#g†4*1Tb+ 6CE]HP4B܋uߌ|,&Q E`H-4Bn*߭ptFr56 4H~ܘϡ6F+lio8p4pvۧCvMʋ$"_/4ڑ5~!O;LJ`04p~eP üɘ`={l`4}86Ov6bՂi(6j4p}l?8({l*ߔuN4B@&XP<ŭ f4o3A>dD|P4BACSsCԐopp4xO'ݦ?Yb [N p/CMm4xO) >8B}܇l' D# 4OJ0$R(]I%Y <!4S1T x~["@4@너(;BگP 4ODlх׶t)T4/BLjL>!鉖]74xM=-d~1/Q5JIxи|Y47\ӡʍ j N9<4O@ɓCϤ)P̙wy hP4xIG:Vc [ݯj8I-python-messaging-0.5.12/messaging/test/mms-data/images_are_cut_off_debug.mms0000664000175000017500000025527211707534031027005 0ustar achiangachiang2112410527tdpt@ajajg.cdm Picture3application/smil/=Picture3.jpg"Picture3.jpg       !( }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ?H1Oݛq*ppiT'ֶ~z9W(RrF0;2 zTEGWk]T'OV▩irrHvP$b9ˀ8>TA璘8&s'H8=}G@qSV__[\U{S/קn){~^]J?AqSqm}}֊rRl ;P0 t*Ҝ~)Bcw;fFEgw쌲09K_9ɩMqڍ2ldf"-J[HTr=l;Ή"ݖ$*Kc'Ut ^Drrhݛ-G$l?G乕xy>+gʕח̈Pm&pm$t9^;'YaE/~'yhεRĺޥ|,:ƍWX$l0ϑdY_SwP]dAʑ ҉GݽstYӢwJzqW&FL1*ZVЊpMCOlM&{ߕzh@J(%e>{j,3Smu%Z=Kz!Lct?ΥKuIjt۵ ) GiP'w/{KmN0nCq #v:UiiPF\%nRZ"k[}J3(MZ<󛌎=kQoMKPg\ak?RӖX5>d~\_1r]@]~agWIBwQTpWƌj!cA ;-,RWL-2:zw((L1'ޜW; iFX;zTIX4'ttZQW9ܹtI*/𩍵fJehm̋╣ mGZǗ_qz]Pw[ۛGqޜy\Ljw1vG2"Aȅ'Z.b/Y1 A[I[Qw >97^k`Qc%^7qv4UDVڴԞ"hi t-m ȭwҏ;?gt&$)Joitz%ʨ3bpr{qҝV-N{۰yӐZG{]/hs`:ub;RP־@]p9wB9]֌VV: ~lJzsOH-q90W_jϨRpz}jF]X:>kbonO%F9jxٛT/P>w*6W.;[{88M rRErBbzF98ⴂgcTiFJznL:;rvom35IJ_iJniyv( ׌g0} ] ͜XZĤ3##_בfitoO+Ӿ[}g# _&y(47ounv@;Fk|I( [$m\u_,tV[J[҆u8WE}4%K.1~+kKrrI,l &K8&RuI{dpA$*#^]&{+FAtkh9w{E^1^X#=ӌl8U՜I)|@Gٺ@$ּ;⿃ͨ((:5kم%hy}ǩU x*R˩<;#Fz Ok֢򂆚2}m]Fwz~1D}`m0vFTKjr0wb])OC[␳j1 hTq*b~ԋSnܿ/g bK=Jʽ6GOz{_0jv,[. 8?Hcq$zW7h} ڴq*>*=_]7VW "`T}ьPFS;\c{n)yq#$dֽ+ߺGE{lTL/aLf9Y$y4nϨqšRLtiEIݽU.$&0rG҂81ZQiz~r޸̸Y3Y-|}})Pnqc**c5d_01 r͞)IO, k'߁n]2R A90:=xN|$rxg '͐?OW1} *Ty_j0ssֵNܽ}yoiVoڮ С$ qD+5qgaU zHwn'.itEkħ%9ڭٕYBʊ\g䞶3Ζcxk5X}fCʼͣwfy8rkG BTr-^Mq"-sF)5kVtGnŒf+)4a rJ=Ū#Ilq{`G` :\Vě8氼)TFNV[ltӠSz=2-[-SF^@{TJ)7ǹF)cf N.Jq J} CHEI2*exWItW`D@pOʥhܗy$-; JqǾ5v=P>_42{iS*yCXNQi_3h5啲2:ʩkv&}]IH).ȇ< 9U5@5i^W&n#R!,Ӟ9ʄ(TZR4Wzn*] @`G'kj~N85p [i€?N.@JsWi~u+/zث7<J/mX3:↝9CgFN9B~l֪RJqx~Bhr\\;][cHoK].nGksq_20RԎERSV;&wI9SWc<YKqFkhcYs+C\%UQ3UDhvBqJ/]Д&%e{tqZ:eo4K;+lfP)`dgյ2 "$OASwsfӘOOeR5ͮȹm/td8b{Jn|aRQ@aU,rڣ1OLǙ kgzqu_`x'8D4SCJxIqMOG.X9_zxĀepi~[- ִ7ۯ`x\SfSc;]_D]nKIHE~s^m!d67 Vk)JyΖqukghD$Ҹ[IIXn⹧ꤚӿzLE*q377$`83ڝ$i7dVY⮄kwUϪ>xoGH'0}kUzCWwC*+6%G˷Zp#ws lNP2{ ꨦDErĖY<:V-i-Ե<6hsU`s^&"[~Qe#+c^*l!7w?a˫CzjǛ,c19# ?Qꤟ*{N_*CWghiHw#iRVfGQJ%:s+|QXq;榣Qqz'c͓jp)F'pM]%{t5ͭe<x&4Q$f&V}9V@fgVHA)'ͨઠm\wM`,A)ӑsҕ:LJF#b#o#884f/u-֬ x8ɱ@]\|rKUOA8psO]KJ?Sr[OY)S/@" qV"IkЋQ-,!z7m>uth{Z/UjP@C`W&"o= QuD(~`qJ<]& j7e$dq\pU-~ZirU&ACd3Os_[[5eR2wwkOcX I~8N?R< vѓ,$H֦K`#zֳnMeKQ 9(qT6t&N/4ԹHP1W|%#)o)ʷngGeiu8pvJ5\*c'\.F،^8|ic?t$RnN4PA*}*a9=`ҷ?G?֖>Rj%Ɇݹzj/M{{юIzgUNJk-4FrO884Y$_C{ g A@IR Sh@b{V%_5&k>ĵ y[FgwcLm(3;/L| 7;>c?:H0z(Qk=WkOpRy#RF&u*ree,C`);.z2zN.:L0DR3=~֔'JI].·[<3֯YyC+㡥8INM"_@& 3,it zRIZ㥴6meRN1e3nb5YIKr#f#IU}bVPLe[sΩufG,αe =C#(sޟn$طg3ڠn;U^On[<*4<̘ -gº]*s(;\չKcnդ[Vcҫ:+{rfO &pA![ķW(@~ɨh#IU8on_#c^ȃ,WϚ~6bs֪i$ЕT)!V'Ui+~-XFM W]9^?zv>gNqz#jRVWc\qHL`Z\K[;X"3=} 'bL>ԤM̓f8'tR2r<0{V_[~dGMZ#,l^30ʎ=$dIxZI,2WTՒzoTVFSI9}RJ~e[R&ȿfD*O#=@kXD- *m9s㿵KmۦbQN=)Xn'JWZLOlb1'Zr$Irh-9q[ی/L-u^OO)Z唋q&DP:K~ٻDcrÑU{}FGaSt'G򗏼;kuM^8 x$@v1 x|o]?}kV^?? ΤWqiW6S*,G~XM)?_ z[ûhe3ckGMvHm;3\k_y-˒625-4sZ;zͲoऽOqPlp88_0sF BSkom228IF{ip֋hÊud0Ba8!*@jN'D֦מ NcS+ĨBIu gS]ȝRWH'#+m# 3 Џ72|E<1߸;{7( 6)I3EN"6HҡN4J[oyZɁGZySs+iEMt6Ê(щџ_n+WO}. GRjr.9[ud`e8 g֥H.$xVKj0GOB y<~մ\OFp̤uuuim(2GDw,2U̫6(֦֟MٓrI.1 (ばN&!Rrsv$rAH'R+ש^vDʃHR lc#omVH#@ pZじ,995Kw ~YTg馔7fɖ%`и0jt@S4s7h+X)3m̄+0QAß+gx_q&^f1.fv?W4˛h8et[o8F:S~=6]W[pb< 6M|?jҜ|EvRJI{/?省>bqi%Q?x`=iBKCZ!qH$xd*63**Bpo%n;VhlYt89ԓ]nED49){ HdpSd{fSlubD`}*؍y4w {XݐéXֹ o:榕9Is=9"[ؿƏ*vczеI-˵8[.Tt:##ִ44 &yc)+u0iz`8WEa8*޿1{z(c^2;@1^-ݳs4K|Z_w͑Ws< ᰿.9;0;A=i^:j5@ &ݽT21)JtO"B32)Km2 s iSSeRNxu;mW x^v1u<u]ov%h*kNKvF!žݎzU)s^/+FH߂VV=%Gy9݁dszuFcǽ9GU܅ @U u*H'w^jmOK~hhbRpJ-ҫ2(Nշe% fGGVǗGӯ=+i8ݐPڬBq#y'$  <JPszW{ْ`b 7"fsA篵^[97=ni|oWPÓWD0rqqZ㫺SV  0G u*#>O+qV,,'##oz2>euMk|}Dc柈"{ahePy *|Qd׈7 ZT{vG!WEFXAZR-l::zbi#v<' j r>':QU[^ǁS97;آ$  ++)0́C:9cfH!\sߠCE|L ٠(l i7[JmVO+r$\\uGF|,#w$?iR&k#&cYPb|ϔ}u_G  # RD0 !5p>ohd}vB-tfy&ș96__ʮ&!rSQ)%2[nMa ^6eZoHԜr>dʱ ZZ꿯QvD>R''VoGeNE:핶Jwxy%ŢkD&YE+Y8|<*ΓjyUvu6 FMx h_,/Mى9Հn"Aڜ,Oԉ9=YkkXiϦq袙A$ҏ=5$ϔ%:Bޣ) 9isܳ V_ҧ]2il_%oR$j CsmQ8.bA;R^zu?Z4-]R]ZkEםswS&aW PJbdn_jL֕9Ŀ/go?Ҳ'|? %5+bsiLboT1O_jهNBZCY߳EKB)4?j^&u)6sxr?[@RcG0~bR0.UWye|]ٯū}}~YrKk9-cHzc(XJIuҍQʤc#G_Ң^zkQn{= +[a,FZ0ǹU?:m䒴*.T-Z+s~"|R.d<3+Nd>Zլ%I!8I3uZ_-{F,f3#ϥlk-2 ~&tz$˛/E(,sJmFnVvVp^;-¸@W|$0!r r5i~ΕGa($jSt_ƻX[ȅJWwVPSNَ3JIRʦPus}N8eQP0VVYÆ0Q%x"rSK<H9|qߎ/-ŶltU|[$F6(O ( FPI%fT-x(]/,cF kJ~mֶH[{,>e7ߑN2UW#}kzSӽՄcbTd5kKF2ם4+c}7E5A ?:rՌ01R hzo3c~UZ(E+ YDҢ5PIoOQI`jx+rNJr1X( ݎs] II'R_yj^4w $<|)l7}?T#GDihPĪTzU}v܏v8|S'1'ycҔKTzsN ӻ{ ǕFR#s1C;ʥNV )R9J\?Fy}jd>ڞLc$ A-ۿ*SWՇT8$8SmKX-zV[WѬx HR cI&rH,O?ڜXƹw{Ujk>rnf zsR/g,vGb99DMJ8瞜ӓoE<׮{sTCk{ "[ӓWPrstq` 8W(+8RLN=SCHZ]n9kH:O Ŭ42D5:.e8cW-IH)Z=&F#@#F+Z t8(Nd;xyhJk-Bq֜Vlȥy瑞E\y?$EF%yj`W/x$渱u6u}zzmW3>$4?[em eG 9'WYVTb+oY@rO&iRo]Z.UևEF9|R~/%1#ɼ`5#RӜy,d2s+ [Z*>+G%˝U'hhi0[ݔx+ltME8HT{K)Z4Kn |!dI|Ap"C!{ /H- 8*go:ܱZ]&Uw[s(+ňeoPudCbitʫ=+۴-kS7!j>4((<1DLHe=j֊9'⿇1"|c88⇁ӵv9skh^}5F,/1lU' g#p]ՄI?o)d{jScAenqҾ𵅟UV0&\+Ei'G!9݋~$+$z&L嵌Ҕ20)daQJϷl⡭'0>[<1ߟjI$xŀDR$ϯJc*GڪpRy^,"0J9Ap}9jU}R\Zip,wʭGlcg]H沏,^Wѫ}.Hv:ۯKr g整ܚHՋ9$g}*H2/!g9$pZCgp q[E$엟 yX~}Zӓ{db9bk[F f H>Ke'mϔ'#+Њ#jKKRd]Ӎ{o™"e!m_Yvru漬Bԗz^_~/]5/r^sP 0]9|$-P뒄zW ܣuMyhԢlSX1,n?lG$уsmnZincVD/#ְRTa#?*~BJ#я'<| MGĠZI?aA_JhZk`c[UrpkD=(u>߻W_ TVGPEPYZl@݇J;I6ta^ɞ}%RlڧBz|@h I$sTqsȂW22TۅRNXrk[|MR' }Q9(VY{G/ȷ)c{g*վw1pT_jWm+v&S-GfybJjp15wtU6u4Tʈx= HQ ht}W]u*I?6qI Ojt-efdSLFX韧<ֱŴHx[haB9ڜIb#5֣ʨgӱ|88]W_=Z@^ )ϥAXߚM7Ѧ[F Plr玘"3rO3sѻ1HڑnqJĎC;d{eK;1_z}hgbA9jXB{xrюTn9_jFH@^>jҕ㦟T'6pJ aPAIFNG#hF 'TA܊8e=;U@Cʓ׿_jH,gg~J68瞕VKO?:bь{V 2 'ڄW7&,n%-'0%KǨ4>Y=? lp,`}gP\I@Ď^V^Ѯ+krhp"<(鑏_j3׫uH!v3ZEF6x˵KɌr+ԂH^-5lA,nxUoHLV7rTIȞAU8a+(-g ?h&2 S+C̼Vg>NkϜ:wio>KwqϿ {C$=H\ovj҇2 Tbһ7BŒמGsVF*]byp',s_QG6H P췲ɗ}:Io=ފ ( ( ( ( (hJ^s_Z>2ՌVv\p@+̦ ̧$}?je~gׇ|ǂ?8Q)J<өNz,Dq6[NtujP|SurBݏg}XĒq]><t$7gijx{yz> U/Yy tC+y߄@ۣ>YЯZ;;^[{H^SSlðjɟt+~Rӎ uy^?l[|@H?Efߟ&fշy +b.q ˙i}!S;'#b݀g4رh ^k>h׿F.y#NLe$8/yF_&X`ڧPIQ;ӟjr\[&Q@J@{Nm6s;SՂnxzU%)_DZHۜ752.3ܾ֕[Y`)`S$eIrJ.d@S# }*-ݾq;j^A;G}PA g>Ԧ,i) o`=:}*T @^oyimt0U; 'ڽ5ߛa-7IFI/8Iʚ~Ctb6k׃wU%I*'oں޶Ex$NL/;\5 A:PF4p0Nݟ_jN,D߽\ȓkevlDS'Eaq=A/H ^%&fUm"}qZѨwqJ."kKiY)&aPHZJGѫJ}{ Jc#+;jo/@񖦺w9OzF[Z}ӳ}{X9F5mszn:^59kبa^,v{~M3MkqFSZxci OpBNڶZ„*+0{} {_N;58)[?[Y@JԵIGF?>qV7U;E4RL(>e8gk(S2u@>>Tm`ҡRp0I&y ts%J00j Z+Iw: z#n#Y;Y?e3^OҶO |((Z pbgu_^ ^a-3]ѮǧRj\~}vG-L?<}6k8IWe>@E0E~ I\㭄uR_c۳gA40MeD#8oΦyҡ[IY_|u8وRku \hwf[|r$WJE*="Yxef]ύt;U &tU[occ]*Vo߫G?}wB%-{E뜾W"MG#UG RG\v&SPͯط1灱Iu_xķG$_ GJZ_t)?“=̞'5ñ;y5$)I$ +2#i՛s`PFsYT?RՀ bK}9 CcM%u<[hI0RB=*D39'oh˾uFړrYPnA2IIڵU^,scjn^ \E0$8}|1睧$$ЕkX{+ ORaA^VD$=1.A y>ڷ3~K O_*Ճ$cQ~emD6BFxo cیPЅ/JLzgڦĂvQϵiKWGdv WoWE iHC* Nu'ڄnI\4F&̂O_zn c^*+|X0 0 OLW^ihpa۲voCڌ=63ßN)sBG_^7<L6ZFJ53'.n&-ňWZ7W|W>I!K<' + rQkw0܇VVN~nǿFy+Esu2;^+Mq呄@'nO*aEݾcѓd:Xh=sZxW =dc^Aɻ xzoʒڮ[$+{\YxjЙFݓPGJ٧ҮMޡUyg*m{#Ǎ̦FFdz昬 +ZJ<Խ䕟ޙ̢S#O^PrqmaiH^o)Ikwi"rYZn889sjQ0 7cI/E?G[OxZ[9$m^+^*jsCPɧH9Jk(KA!=*DTV3N]obmLKN)X8te4b8ٽ~\攩FJ]ne#pѰ 'sߦEO$"SꠎWDZq֕0;sϵH9P u㞔Qr}#)^׷ 2ӯ_QN+vGO^Ԕl ,]pSSAUb1D\jg{`LO0=ϵXF -ׯJ֥ܴH3'Rzghm^3W.o-+ԀNfxf&ZJCZzr:nn}\9ǯrm53Zn/RFZ{' zڭ5hYꉡy F1zT0p<}]7o"䐮~Wzu/Nաa+A4)_j[z\[>ڬmQq7j^_yS73Ik}_ww:>Q5gkwڬL@ ʱm}2m( /5* =8Fq{yg3,&٧c>ZnuZ}ȳ9Q|vxA?v+nTWL ZuocW;;[Q!?j\ոʭb=xVŪLf?;z{^?xɯp&[aupK O~9'ImiSeDԭR@bLs\}/ػ):-![FRSk/ǧO/_r蚶 ]^*nA%"*ҴG Ǟ?KSbڽZ;EEz_(KitdPxmr9Gq]j)]ʊn/MZ 施Ֆ}irzKR^+ػLQ'aތ)F9t-A"CpKsK̖юJ] һ^`uӵ*hԐ+)tId?V,]m1|`2TRp,JUUXQRU}M_i(xoUUr%]>+;kax-=^qv-wO|&M? ?1owx?S<1xRE"ęēWdd17ߡJF\HHwJߎ7@C S^kESvKU0?ſؗv 70<.G9\úo\=1U86$y_WNWbM6K VB5&ͷoj/zX|q˷!hg4Z GFw0gshmLr~*R }-׋:D`f`?3ZxkMKHYT/:e5&x*ڤ dH, >y+UbًɌ%n8 3M{.:u$ȼVв8ϵ\Xd >n\j׺%쉔 gz|? p#=[N僖PU̐H?iy=ȤsڥX0/zqNFErP2[Hpzd'] E5'zy]-RB:T8;y꫌{?=;ktm^8ڭ"39WZԩ3ֱ 2`Fsڤ r28zQK^C;NqV@~^HϵhCF>]t~8JK5/  xtO[=חOP|mgF+;q 3Chsk:HAU'.TGQ:O; )MYo,8R{z|C<QW7կ'{ݭRᢚI](zմ IuNo˨VhA8u洨vvzm-ex'WֵozHnb8iyCӑ^WG-Φ$Hr]2{4ٻjɈq;uߩ\+K%Du, *z|8":hg6h#։is+~vz%5jR霯$KcU^-7.ER>d )SAn6Q]|ޓ }yatt18Q~ts֞Qz{QsMԖosri!ºgҷXu)NZ5I^9Lp3 i鱫ZhZ9-5a%''Z& ֔)"/"4`[[i(qysR7>}iۙ܉h*⎮t+擠j!3t<#~7IXq|Z<;8HIkӍk;ϱ_zߌz+}> ޯAx#Y dUW,%(Z_}_#Z{5WlZJُR)`ǝ'O0Ne1ºaty4gód]!O_5X|ѮMb#d"=wW X.ReO"J{yDA[ bIc[n2eU]O~Υ?h,ZKkTQZncpƚ-UjOgHUn֟{.!дeW0h5-eg ~gKJ%0Mz T W$rVZWŹqVX$ypx;sޡ[f=AO^}Fֶf9nv`KqׯlVm;>~^kNvקVVEFqUP`A)㯧bIx TB;jzI߶+Hr]uշ԰@qz~x$zؙSS,s)Y7}?!-ly-ºzr‘?jWK{hkiluT$Z {?OO'؇X`$# h~ |A1M'NcA7@kXATN\;#j_:zk]n5[Y{qVMlxO5{?kk44b׏-alJ~t] Y;dq[ҨO]l}{BN/_DlpT$ֲM2\9+JnnQ;G.+vqG9/茽y8Kӷ>:u؋Q;$qT=v 8S&u_矏ús7-?̦4ZK#lP1tQN ףD}گT&V]#kL5c-ܣt NxDc86XGsUo&ѿ;؃*Xvӽf[E\]Kwo gƺsa yU;IV2QpH`HF s>O`n7EBF=>ZvӤn'=:rܶ쀱9}eߔ `=VZEaWaj}}{Ķe~Fx" 9~k"SƶdYvtjn Om6vSO zk.܌c۞շ)98 )(ݦI$,DkTy/׍(;VbIgeF:e8UGnR]캿USu'>u)hzq)|>Ba_˓}~t=;B>e m 24inޮ~.ʧ*Kվg~.Llښ.-ij~c'=VVgTdm+w`vz~w#p)=+6A;KxVU2A?4坹׿_zI G0Dmι}:'iF&$b=|׶~(}ÑhJIۺO30!O^1`4-!tֹSU9%^ﮛzSm¥;.q M/]ha2WHY3bMJײlWmO߾dypO/[WswPѴ7$o=@'98VlKS[vHdz]}Iı'\(SD=Kio{bcZB1RdѼ gZ$"-}jmq >\`=+˵j _aֽW7g~'ynP9銁Փkm'[$̛v>ђJvu gu ]S򟨭ժZٕ8WoM0H2z}A>Wxs:'}+FJn:כR);[yۿ;#v8 c{[ѫe8_I\oڏ MZRɛ֍a7ֱsiMY_NBsۑ\7ẘyUr˞ketOO>Y]X>Y ܱ2zSW&lu.s*^ ly J+'uB7luXF'_n+f0eOcyc%i4LO('#ϵXq8iGZuSnfxե$vP| }}΀HLEaGVkt[丑Jѕ 2~ 8w+umJ΃:L#,4]$*\~>xAMm6q>w㷵k84}:~:y8sVQߓ_?CVrF~S#j!c>/!fEa8LR<걓o]C՛*iEE_s~3#W m}-Ԏ|Wk3XFg~K?_ֹ?kkY=MZMkgE-Żj}/нӭ;xW -]7n._uJ:w;=Te~o]dT %BI<?ƣj# n4kX'H)[_K$Me`^YZ}kk{bG ׬; $#"(qd+1o{_HCVӯvVIm5[Ahm afͨF-,sh_m>dacJ.ZkW;(STs {57ֹܽZ'* r+ .`F" J)Z0{0 cwjk:[#֧fūai3wwe.)b(ҥ{}BG8uiUHL' 9-#.*ӫG+;_;棵_>"o 7F#$(I8'׊1_Ygm?o/: 5+Hm$E K> +,c'{kSty׼`kc+@G)IY꿅LK^2{}$ {WD9Sggk \έ[ż.ӑ82WOg_>U".Tw;z%֍yL3Gv6*wGmFM:Z2ݿEIPEd#"TyhzdkHHoɜu)/7hqUlfaA= }(ywf<sI~4[sf.8?55_smf,HSy:V! ̓Gc%)6=W1ڭa!IdYҾ7[jZAA%UBOsU5Q8.zoD+Bn^֒鯟mzG~&xfn.ιdʣ*`?i5ޯi1 Js8bI䜎v'.hD&eweuzLJxEE.V޷j];tߡP5(!G[y߂T;xS%ƥ' &־kVW|2x~IklfCpniI--Mtk}.kJ3TW65%ݿ3dXMERZkMgx:goӝսoX<YZA%Qit_?w ҥK9x0WߕzLf'.OuY2p u(0imVV/F﹞ T[֥.n6{ғi_cZ 1SZ ڒ\-,Ndhwt%A`Zl,e½03W-L=Nm{cR_:׃X~)N(PYcWjq0rA}}p2 i&osw^FIKG 'TzpL?6^?ZSʞW_r;t z OӺ­:0IUZ*̥uXkbk~3SAܲyg-"Ǚ[1VyvPi"дqHI1^efVSQx"?~䓨G&y5*+']Q߿ri`nPz7oUIZS”g#9j].tN嗐$z\sueeoǗ >VizҫJKwE5RYzNedqۓ[J+ک$ɚVXE$;k4[JndKݷcAbM0:חڭjJbz'ҦQ_2?y_#7eP0I^V1209>՜Rw[sKR1P0:דP̉q#1ZI[iF=zv}\{v8K|08>:Z Jzؿ5} ֩Fs#LXpfz VqW,yqru*m x>Unsz=0jGUJӖ[qnI$#) ]&?qidǁ~u#ž*߈’v9 Һggn "wŗKe k/ j$Ҡly9>eR*3^WZz3Z1_#y:Xi]xUM9H1xd8?pT?啗wf:}MMRi]m^b3)F%pu'Zg]6@ZqԚ4"6G}ddXtח}O1+3>?-$/5KBL@a9Mz;嶝3<=_i9^g E,I#=l mnRy1V*7Bv/jl1؏NIEf'D*pY  b!4xw4tKskCF? t۟7Ɠ@kURIw?掼W}vtzV鷡g(ӊEMK$20iK G)7>.v,8q*Qu:e`He<;H{ԭv=yT#&*MXLuEkO|3Nh7Tssh_GV3dБeqׯma',#hgլSm36qqrmJ8m&ي䑂xG siR6z MyZ7ߓ=NITIaZ]6ehF+[;w]ϸ/"8/LA\ԣt;7ix]Ò\ xys7M=orUMڮC ,|sUF)[UF DsZүJAnzuW3h}SGD^8҃G稘n 2qN>.4KXI$TIyUJ0zˢvMOZP󌬝j+T9~[Y2=^HB:W9Nn}FݼXl٭(澫Cц歌 rG~/&LuSY\{wrjAOЧRζt)D ֮!8_'(Kux4.g'SC\TG y^+/#6`.$O,dTYEw42((#$2z/f7RN>}^嶏{Xi=KkC Ɗp+qu;RUwv>; NwzA'޹6Zk;g5[l\oLw_lE^uy } h+=HhЭ"&# *\tT_:R>^$z/oseB`IRM/ + g?XBԿ,v +Zw2Cnk[o%Qz5/ 7О?hnrŬq?!%<='.6)bz[mvcmb"gN{~T] }o3YIt:Wcxb~ҶMF0MYiŸ-}$EK?׋֫% Z0:< K`W֬e&4ïk=t@zc4g˙ͨ( B8GU9[Bs(RH~iplb&m.70yX6;2(~_еc#P$l:?!r>^O#nfd@IX_ji <sWRpWo]3np[xzWxski6ĜU E9=rjơԢ<1AGOFP 9zU(?J1?|yJd폠yy8; [^"X;꺦~zdI.Wmt~ѯ#ZIlgHd%[<(ʔ* rA}tQo>߭^,#EaJ䏯:UXdgcV\Zשb'mFr0+Nڻ*'+tZ6%ۂ;[M%'N<> A hux !$k*麗aO26?Ąr:{T>Yʬ7&[|VwӚ|s6mWo7l]:D]+m.yX5^3EƧE1+?h3İF 1ץ4'iUx9l]8ӏO[~񉑆\&qq(cSph6If=GBO$[N3xRQ^WΛ,%2 YVYs^i}=%si")֔>/s*41qcZ.M^ҥyZBb^G gt <{TRԲiQ4lȮº.p#܌woYmwG<'&3^Kr؆%\v ܷc\W44V8e9s[9LJW"DQrjj._vae&?JLҴm& [tz˕'oғࣂO(;:)N2w$!~W!/fi=Ռ54WXH2p7c'4RSb:}fwy ']e/WyO=j2)zgIsm2WM_fKRu8mǨ^z_LAncuMBmjAi5ߙYO<]Ni[?ֵ#OSoO멽xU-K cL/xFZ$(6Չ>3x|-C'oGɹow3(βnN':u&]~{@id.G"HGHr8rj(-ae5EUmC׌N+J% SgAf$;`:{V\1m_ת9z^R/jFŸ+"9cc(8I\USkɰ/ܔ{7lo_W2؍X+7%ʩ~^jGO]ϩ}^6\` j☺b|H#ѷWe9* a*IǒZo$X~qlv%ͩNЇ0QK22 i''_'1ͨcӍ8kt`Xm9-x>+߳.F?\~-Xُ;;;:fQV xQINyoC9a%ϲzoR ~Wyi|R:s\UՎ՟KH[M_r;=GSPm:^[1;[B=7/Ta"g 8sISZԞv-?jΦB@8*y[!hY?{8>se8z/-#^;Ҿ{2rv~3\N*gOK/J9ky_E8h` &mknRd<faNԤ85~J O'S0f0qLݗf,IJ"\jkwmkY 55HEmdOۍ9eTtVr@Pm$WB$읏%/O!:˕yCC2(^jRs5xIIW84+\´&8+j M1S%;ع֊hF"Wl``һ<˩57xkn){QBO/8ߊ/LJKy 3_kv YF##ݎޢjNבRIGx+[cpq{t9P9pRsk$ NvtPpTQkEzhdTCko =AVhH(" cJ];5?CRV@oе-TY#zL1\E֡ȵ@=ڥ;<W(8)tGxzsZ9˃WhSS|ϔiŒ_C䑓2; D \}:V)+5}D wFzbZQ c&Nc vSe!KAf)zq۹-\?tz{$oN[SQo&yp1rO\JYȧ~GWK[5IC"|R[]n&%VmJ|;hd7tTiۏG'*8e5Z1#jՉrN@QҜob}{Ƿ*l=2+COVN>}9r_9N Z雓S]:Sz PqT|͏6l%ʪ\i&|H%I>}-#&վv}6v-$- O]Ϸֺ EԬ.tix 2qYV;E:9 %L:U4 iv =>'ʑVVGѺG"E /,Xq[>z%:]5%7졣!!I(1]Ch^~m.Yơ2+z3ʕOiu0o%%OGI0K}}?ujzu+1qV?}/ұQғʹs::uLL1g<-QG"T'8翵_'iB3\3G}Yu5DY y=}6\ڦI+>y97ONMnoi+\`tZdj8+U)zC'<yrU|fGFyKJ/-MU]+*i$[6sʔcvNO7DʳNy( l~MKǨ}H[cQQPnvFx\~̤f6cu+Ў%8)=ᙢ1Os×ⓣ8s5]>dgQGy^eXf,U]mEgRzpiϹ(TRN4ح\pG=Wx-eC+AM);_VcRf;Z \Lq%Ww:ilԟO~sI>ߩ&fVyr.fZXtg?TU۹])'[ 0m|-;iBN;~Z=|8œZkQeDR+%dմE}Wgt=^6 ng YKO:^vQyImrp{+`ʳT~׀uv>2Mw70Ɍg{'J?_׬"p6K㎕NRo!]]?)1{RntHU9*&O.2s(gNZg*3O$ߚ=\LԻhFkx`033^g>a { Km2Wd'潷6 ;^2 @l8^4/"(۴eү7̌RP [o92^).ȀΣ^4$bTaV䝻0q)c4Х0I'QF7n^l$pp84S< (+=]+v%pٸx$Zz8QMSO*>_y.XѶ6Ajc.27p1}}m#}P3_o6Kkˌ_jE>8i-UbU x~+v'8cM+bi7υi]-԰i Wq_JXOa}K#2=&xչK=t^ΞTS2Brߎ ;}̍q^)GdbS6HVkXFO玵G*s6 q1m6w9[Y/@H-+u4 ɓ9t yxvh`>΢l<^^#tk[+>i՘UҼ^[yZ:2+v#\XͦU8{R߉זV@Ⱦ[ezz')mrqҲ}{D)SUxg[i[MI<eq}76zM$Qr[Zr_?9f/-F=Rc+9/(sҰd(P>ߢdZ͌YjVAc<(#²: w8#kɤgfql^Z" ?VM֢dޑ d+kcQnLFY8}_+Tt*eH$ݎE[3a=1ϵlH*'{ m+jZɱ1x&jrJDod_2nc[pALi- X,@ cZo13JǕzѣm_2>VƚM5d-8 W%NZ.Vy-y?.ϩ*Yj%wfrڡӪdׇF;qϵɥMԒ7g[aC~Y6zLܰɑ=M}UϭrΛm5,Z^T?w 7vNFcV1S{u3>sRG]HG݉kov~G ԎUi5)K) Qp"opH$"ʼs ?J?o07K[]񛏇c;(8D 4LXv:uryiM z:ʛ9ggﯧE:_9G|͏`e"I($dsZiE{vcg)>}+no-1Hε=ԌߐųǷJ#qסJ 'd~ Al{VgoIgy~kȭ9M97k#f.枘 qK)#vϵ[W%>hMm:FR۳m?NA[:NnH"hۺOיV n8#pSڛxBFs҄I*U#.zhGiٿsLr7+}kԵIfi LPLֶ?{m#"poXcpPAJҔTsOc뱞.E[1y<+9Ұse #N?wt2^'oPsJaWb.rFU=[[~Z=ۗDm4Q򔞣vyU_sؐ&1: v0 d>)%88Yme *JH|rx'i|3iM_ֶOMeד׊߷ d< pi$3q5xX[G˞OaIǕ$SE۳kάT+fOX rso_GG<[O*uњZަZwjA-z}+n统}̄s\|#䵻=?eV쿦ggQ֍RԒ`a/^>h]O/818imDb"iε+Kȼyî^4n77on珉UCqZ|Ep"pC08N=+ӢۄrQ}[|g9^Fr)1hKP8IILe^`IšOK'ǞvJ\5#~<סl9 OFM- 㞘5D.=Jtn}ngfR `& &BSHaSoY^99$/a꫌\u}vfDžJC9 鉪f?ZE)k~i0*֥ wqڮ6\ܻXkyL$^2zfG*$kUJ,1A84HCir*}ԋP5w?՝ܴ '$p7edMw\"PG9qXIBv{_RJy '?/_j̞f'ۧ9rϟv0DZ۹}(0!vI VWй&UҲ5_Y3G\wx~i ,x@Q׿\j_'~%Rͷqa_j[/.Ҙ tS~7^-t4-!onأ8[iIgVmFr+sKɗmxxeuyhEmènڈGh.ç lTM&}޳dH4$X\H?rږcӠTԪwH7 35j![p }2xQh&Xb\{ӏm\'͎a.te@ Dg }+?kM;(tu~VAޓk 1I}.h[m_k[eXWŷOSz5n$ܤ ʫ#85xk#dG8sR72B3sҳlJt&[]xo6ܕ~ʜ;5{XV]%m2ԬLTx?W "1wzo~Pө;uzw[V,=O_ҮO"H=֜RmucFzkk,0s']f%, @f.˯qzJ ;{47]o@pDf'e5u\UĪxݶz9nkJI `̌!*>hӟ74v[ٳn{-qGkStBrR~u90n"yΣ?|@rMv|"A^vOuwcjk='lڇ= jvUO{2n+X+#Ƕ~iS1ךO:o^ygxZy6sZݪj/"<⼃~ ie"SiׯL:)ͧc[cUE{&L 8978/v'yރ&e a%rw50-beU<}kfdqBѯrƸsZ2O\~t9TIȿ3#eo9OM9_&"A]Jybt63ۆV 'ۥdku;>8ab9YOroRrřP%J$V@9#MBI_{7c|k|%b9O7ncldYy}Xw5u6vSЦ'[t]yytb28|y1 ޜU? nuc}ѻZv?&]UlѲFoNk9F.1u#R} KP vW=j:N%[֢%NN_޽?,#ӲH`:i}w}vl2ImvDѥ < 2?+6fe%qϵ>ȔMgI܏c]{Ӛt۾t8yғȼ5˳?2`G"0$]Uhf9a\m{ks_tjZ22A0kN̡mE\ѤUB1[=ys^}rĈm3\R_u*Ei!hQjz\a]T )Jx-ekOU1TN}e>Xm:DJ~O+B>=NML$J`ݪ39NVs/ɞ=qU-Fx8ٯ1l¹qeu{jc.ЎY 01{WqU-!g$:N3zU%Օ{ V.W\}:b$Np9-ՓeF$?ʨȄdc G?.qW 9+nmg Z\tiӎRXK|wH~б1ZG薯?f]DP8bܝ 7X<ͤJ%q~_ތ~eʊV) ن9B G=ںdTcވ23aT{t51GHa裂9dMc8"Ϙc$Oƭ$;ut3FeHl/ՍeWhxgҳur:"y|YqF6#g!-ז\C!Te xz2dW5(eiWmWvq^f÷zƐsJS>xc{HS|UiWMBn/}x'6rwDgq\{ǟ]¿RLpDaQmx֛{.ӣMW|і=ѴL㺂u!ivLŞ+^GKCc9~QE''-ݻFIҕ+{\<>v9 nLp}YǕ6SٍRg C|SU46<XW_@čWW0IjS.ӡ6{XkrY~I$ц$CLnܗS2MFɝօpe;qeg9=&>+᩿$kɖUk6wU?ʮ)6".]NGqbk*$?xSSWI=??J\wM}{.pJK?%q_D(=jiJGjǹߏ "MVfKɜ^]jzATU}n Eϧ+՗f|ԣ%>Ykoj۲; *FA[1o:WHKm%0lQM;%|w_;_9A.吇Wko|pT8ZkZ.V5r;/Zw7ZT%M ASιv4cx'U'mri[卯LO FAi 1j)JkyneĄc=+PL!I ?ivR..N/7~;GOB:EmΫ*U?ZjWOtc)=n#ےTqsK-ke|~}ssڵƣiX5#zU'7c;$I̵9-jٳB}28.D:qךKvu7xY,W]'gi/khP;|B~ӯ@Q|σ"O4Wn9`c4>ҋܥQ 'm89?ZZ]Kk_ozѷ;7{_ӽob9~oWE>'e sw}>P6*,l#QT|j;Z%>z>xʔ`U-Z}ת:~ZGp~elJׂ%qUjӕ.~{;_k٤QkZW^:jN+~Ock;FŞ6USӖ%MIkPOu},v> OM^@6T 0?lY~ˬȻ R2;'kjMϧkuT3 OMƼ}2N*eG[jC7$?.k4s^G{ t-qc[dx%,Z1G'˖t,G8կ1L`~M1m~~\<$chءj@1sXs 2qޟO[ۓV6VYg(߶ciMڴ6!ERd-5[tr2 tQmnw"UyOyųO3 ҴV(P2FO+u"~_GOHE[E4A'3Z#m( 'Ѵ$#W/t̶2S_[R:;??'|\r~6k顴UڸO  (qn@ +fR>Kz4g,NB?I a}H0-"ՠ.8:WRAy%oF=3ΎZuⴻ]ah΄u YO_>Gᮾ-ڧe%HI@{ڽRWyJ <|vnz^n+ ]+SVv.IK;+nNg)v2vә{9wx [[ȹ)c+i [ev1}z~TSw5꘬ 4m{/1Mu4y^ن[(dBx\-խ;[]T-C|OsjFKRŭ V3jr1]|9皷SR{~s*35GVY'ʻeԭB$sZmdTMd7 r4jiwn*b.y?jӒC8^ڞy.뛗Izyc#!zs֒>hQQ&ov3.d=sOmpz@U73}S/эM}nxHOQxQɎAImk(QmtcZ8^22qVcۓt䤔cJkpJG p3ںD$?fۭii[|~mNR%4uP %3cڸk:hɓ^}kۿG >oᖏ:o tڣ2#7/Q]>)ҝ  L~u&%TV@x$ 7Y[Eٷo0ڮ99欙@ʟp j2CO!9Cʞߨ+YFQnV3.PT ۃ; V3iųI2x[L<8Im8>^MѡyN F:ufrV9yQ\ތul23VAqD#)jԎp:s{t3IV*NJzT?!{ƳveAa95gT[h65~h1ǵeQ{jn)+-->k!.ϳctz%rM+fM </K֞''ʱjÕQ.~G9AO]gzޓwA[3@~Fm5b AHraGp9s\Gps庺拏n=oŇ9κ鮟3t{]ڥw1@J~F XanFz"D~_OxA^WּkrI5͒rfExO4|3&azBoCGKM;~m%o]mki7u-oNBueB*%{F(f0ĩG[ˉܔ#*+I5շ0ǒQ[Ou?ĒAw+qTJz];¼A[CnR8'aI֍+-DZ&oKM$cIS5jw/$D[Ջdc#&i=Qt>y_4&&!8 ʹ$n (&rp@:"*.d)B a◹Tk1kާT?gkXeFjJ-~EdM+F+|LhL%zWRMeo~gѿ2J$FV`o-9I%G; B:~);+zҳ.dֈֶcw8)ȭ[s$zm?ʺyƭ薤ez3^2)\*f+R"~cuo0:K9^,{qJ.oʒ~͹ta-/d.t"@;~Tg+ץNi2c{t\vbv^f3Wܻц)!9K\BAcz0ì}\ő>w{wZ_FA=9Ůڗӟ|YT9JU6nՃhI.wZ6{:+r>bRr/[F.8q9Z6(*QJI-ĎA u[2\FN{uKؒC?YWX"dwBϵg7ίz~WoSY ~Cl~~x^Gs!CT˧_z][o6)c sϵ2Lm<r?\V*Jz+E(IN.?ހ\ҙ<.!M x  #>ZkǙF+8lh8toK{F+KC6Io#c§1 rIlmZ>e^gQ2mZV4>%B`a*@>/}9Bmdv<,Z޽mk5^='J{>y\i[\{s)ԄIR~aqV(:pw{NٓY_1Z,dPUHڭI7=?$#ߡV֍R*A4{?s(MIaŦLrF0?*%2_M*a1hk=:]MﬠV}}+'P@qҳbG+ ng,T0y{FntI\`:j)M)S4^95!8g\~S`uVJӽ=ߗSՂKW$oR QUGO[%n_VwIW+HGNO/$t8UYKTRђ<cT@>lr@Ti]l Wvq, u@$r>PG?ʩ_ԹknRE"2N Z sU7UdxKi=$;bOSZ~JP>#X VA5I~oH%u~~~'=ErћFm^_9E0:s kVAY>Tn6q1,0|j)3xE-cqF-Pc~dJEBjGdiH3h]#9ʷj/j-{.GkoGoiov:46FO9M6TZ㓁.n|S-J}䯜]2>j璏3a.s}Uy.?d$:XΝ@`H8aMVs8dt?[=JkbA9We4Xh[}@"ɭ[ H2)$b-z01=i7Eu>\wkCKGyߋ>|%?ڼT[[qY^.JkKG8#:Jv[~@ cdI[rtsɿp?=*qr˸ܣʔdܝ}t (R5',A_]2Z{ [AVa}Qr\%ɽc Wֈʍ qsVB gںy%LI_h!]Y 8ϮJk yQRרKp#'>Ǖ_U'p>4ꄛY>`zjȢU}좖Z)߼mlx<+h.Е෨'5#ѽտM3›UH$?:=CUe{놸ms+ RkV޻u=XFamuDlGFFTpkkdj:+PdQw{Jr,Sz>VU/64c3<? avS]u*%(\ߡŕfs*TٯКV%|oeD՛NխnП&r! wz4j~wH{:Md Z;hN n.\9kIIA9HJзWoM7u˫Dv'H= L`# ~U~u{ؿ ϔCFv8sWSWtgXJ1frnRR ׎]ׇd\Iiavܤ m}ײ:2A9`r:?—!A'(B9naMyq֨a;3<*7mzˌnb G?#Ty6m1J^@׎Khޣ#xʰXDT[cZ\_n+Ȍr#v>R _jkhCtyۇFHG_je fpIϡڳDȀsc)"ڳmK\1bXjl \ߍm$ٮbBvT8?C_WC/Vcǯҹ |EH9hKv7v~݅}nUܡx#\vf& Uvϵqsﳚt*qjSe8_sB̧̭;‘=k[rɹiQ$9k2F7,T''מ96aqn#Êqմe[t0G?{zVUsN9{#M37V\[ctS̈YI#`]FȣO?P+j~rwr`zsR Gڰ治}WcAӂ^@*dL OnWLy* gZI;b߷'$}ҢJ*>i[MoXu{4.|zSni{8=GsK`1l9=ǛKW+v5?~.B)"Aoqyf'?ƜE=;'^Xw84! U09O<ʽ8>Vd/nw7Ԍ8lcrUPOpS]&HQ|VW;xf9ﮡfgzz ̸ޱ_M%7@?*ojTiȫQ&%v\ѼR{ 9»+F6: Z~Κѯ=7?5TJrZ|oF4838-+ڢZܝӜufDy@=?*,K~&3an <A i[b+\:zȆm˂MG,r唡e~ ﱓW{՟R{"(c8vmkh2+i#ʫ\%9+kUe2#$~ٕ@s7*io~R*mul{ҽauV@qץm 7e+sY]Y,ӯz,Vx+s).K{.ll0=AWR݇O|}+65^ɩ:/tۧp*)a]HµU(^=NM6Df _L.[kw-Tt'$k1A+t)H2u(%K{?ȗ%tЯ-:U[)nq= .+ܷF'nr~>P#91vf2DjBkn* TǙhe9;6X !,'aU'2 `|{ҟ/.nZaLΟ qU/c]b[i ~__Ea-ʬ e7,MdcHھڟ(zjE.ӯ^+zh#yYd(ӮA(u;W=ڽGO>RRoSwh$o^JVWVPsY^G:&g|L%9AZZ TʟVI^_"]jS8S)_ټBvb۴3QTy'=>՜{woz^ҟ/B[O(zUKneR-o_b I W=}X'<\7a}]ohic#ϯnk7y?x r}8ݮmzK|˼D|:w:JKZf!I*P/$`}+t5́3)Mz/3Ⱥ$Vlg \kDkva'S+ ;s)i OMÓjE6mV/'f[${ MVbGlֽ 7m^rʎ#"i2v&c^^^H/gΗ+},oNCSuMdi}<`W=Br 4,_~_q ʱ`kr*ⷍ=Er*U #܏R}2鎹҃Vv{kmshQGVmlqOҳT╒!k٣pR 'ڮM- 9$ qN* gyoՉ%PpSY2JH =UE;ߺ= l9u3݊ko&O#>:[ǩ#ZIAũ/=]s͒yIi?*4=zdlXssQOkSдf+)l9kqzRRI-'+;3/Wݶ o>^ex+@I4U;K̈09ڪà";%wlp1SqB]L/p=9Qߧ6eet_!y4sdzU}2] A'һ)˙>gh|ގ:"ܮ]c`;z>p#؇ʝ}E(蛒9nqܝˁA o <G&OsW_\46A*;NTzzRMZ7\#ܲ6pyAol!,0NTZmC;+[>j6'nUk"I-˩ tNs$FR+\D,r1:(\[zk$6IK!RpN8l}}B(bb(r3׊rv*g\X+OVn M7BmӠڠ@$, ##qoNMCՙն+c YSG$Cb0}3P>it_#gO-e]#A׃-O zAYYT+z#ݧ^/S[Y `zORP=A瞕p2viN+r> @?*_ XZmvYwңy8kW{Wh迮9%+9}}tw0)H<榣Vs~"2!\^Gj=nv+:/"7qtJTTa/^3h4WMbKG~|=kfT pZN9]N#}M7_u?%XA;+OXN\4K- g縬NRj ^ӯsЃߗ9uHz ҧP]@ Ǘ]=hYHӃU8! vqWbu637LD)HU?șEE lG?<$qϱJ%6EpªwrZCqߒ*ՑoS[\pHZؚ)#V$u qSFR7y'Y%یt\yRna_smoC+[r4Lvt{K@v$J g/hQ{qVRv}tXO9=qUܡڦ2M9Кzy 1u #U@#e }?:%3K4%Y[+fFq~UbY Y䍸+[_&KxF#l5b?Zߠen=nlz-k@'󊾐@ Zv-%֧UP!Ÿ^ j-Qԟb c?UgSE>=8\Ic~=q_d?һ-#[Iv&ggmIR$J!{=Hǥ4_jb('R:Ogm&)w"$D9}} cׯҺe]{>$v7\G>0E8?OE)B̲'ː&C9皷҈vSInJ)088SlFH=8{z8̊08Ϸ.E-n}\M]{h:fNFq_jok֗rgrԔjr~)E^ݼ3Z\mrv*|X.6ӎ9:mY٩Y2U .0}sӊ#BN>zght袅Xܤr3]5fzYU\ߋ#RTl_8e$|J[V C8TcF%IJ\U jHt) }AxW+@U ?.8Cqu9TlCF8!OpzTrB^EP) mϯ5n⍙XܩVEjInIBROÒ*F g#{b.Kr?ʶQ[_ s#o?Oڵ-59| qWy&#YEI(ҵ #8Sמ@$E26+ BQ8d[uO6^-S+|WPC @޸fn=ċh3KƟIr7}Pg5 EwîƴPxcuQ]EtOp:ګv' '>5TVHca^Y.%/!cz=Jq(%(`y8 Y!:1ҜZĔv=_f|I.;zI 'R(׿\Iʵ}w}=Mm<>+{<N`@$, vDZbo[՞f).Ht?wga+7x(u[;8o d(=G^ZV}T_Һ)߶Ҳ5mgpAn?]pOXkw$j dj?2lȯ_7VEӷ zvmZp?[fw0}3>DjR!2b0}反+xL+)In]} 0_SH[sYġ=0?$vl 0Qt 4w3hɒTQtU;,|ayS'ʽ-,T3 ӚE(CsOrÚ4G#)!P;q_gbU`1V[ܢݭ71 3FAzI$U]WҮBbtBF e;xVz_3RB=qEpy⢤k_hW, |9g=}h᰻A^toau4c,sگGB$=}}՚bOa*DNs>1dċ5.?3xj* H=;~x,Zq %3G5y_c7V'ͽCúuw0y .,LmaӃ\kH:#4ޮǟi=SzU.|:yo$_.ΝnyJeJQb~DwpQ3*I$\Mq x'#<~T[ݰ7#uƕWב2xU% G%◻b5~}*彛y \.?jqƍxYY*Ļ{TcI)(]},#/nH ǯ+;D)%E~ cڸUbX@ϷZm;!IgwWKh<WC,kK[s,<'h wyFbČzU5y䞺_֟+5WgR劖H0BZB:t;:WtgP!>8jįjYṁ`Ϡ*?:WkkmTa*?O|I| K@r?*~ﬥ\[,O֒svz[s,lHZ*z6p  PN2qQiw[ݤq?eJ $jWvgaM JtEu_7dX%m0}?v99tJy7+e g)3'lg{!]hYIې?ZbNTZ;A^#՗#H#HIUF*f/M PH/$Zpˌ1Nҭ;qF-ÝJK<) ʢqOs_pnHztMgL| ҟ<|rT+@ӯO*c G-%ز!wq@7` Ǵr% qf@VJ^`Nw?*Վ< )-[Qw7Nd^9UIh+;+lwZZYP=*Rm7eN].% oS2dLj6$udVlC]廿[A]+|`Um!yiռJ_ hkhiW\ޢ,zڵm<DW2DcT-צx梅73 G*˪6ܷ,K 뻧j|#+gz|=+{ 7sZ-:ܲ=*1Ԛf$Jd{⧓煼R3~]E#/fr^;$xO9\yG9i;0K`Nh⚇.5 ֐~T~gÛq488>MxAAW)ti\m=~QǎP]s?j\Fcͩ} |ncuTUTci)(rޮŷ+xBn,rqמ ɧub(򠲳p?Sh N+%'ԺJڶӏ}EEީ۱WSL J" rrSv.2j=mH tGO?'/UkY$R,NN=}E wـ=:Ҝ)qяXT/˻4-.IGVdHEkiYIZU^Tpj :nzd"j_3 6dSZXrF?5FPzT[=F{s3ۻDdiΫ[[oZkk_'$7^h>& EÌ[DžI,02m#hQ_Ϸy1d_ ߳#*V$>+%`#p1Q'y5-֏u;IlnKć[6bZluŧKkcjw AרJjVܷvELijDvؾ `vN%wЌMKEo=oВIҫ<!OF4&!΄W<;(n Jq{ j6u*F:jVڑ` q*jS*;xl ֥d>Yg?ϝ"ƣ (O´Q`P-oof&Wnm_T{%Qc-εޤ.WM{Yn\d?¶J tf$ۅ9Z.@7 {cVcqꊷ`G*1ĩ w#/9@Ϸ%9J*V[[c(*XxPFpEϵjuZcZ#_3`-ۯZiUeݐGY7'k}z)0 RE_j%ve߻F=W|H\76ihؒ)Hzi]i&CSpMgң 1Q(Q|54-.yZN,;JO8JzUݿ̡ų{OwXX9;yTIvN7:`#oG ,ehX`kD$_gwaOE[=N:o>e9x奙D:~eO=A?7)(Ml#rtZ"ܐ~ʝ8'#XHY( WRSRRZ"\vd/Z6+"u2h i>Ү) 1U(_{cqlUn'5LŻh@ p??Zv*ry#?Jn>WQFZt2UAr#r*_/fw31' w]AKР0 lBIېIZ{G֞dAM@z~J"]fH #q*d/J е IoʼvOSy,`csU[-'($QoP/LgYcHvAʧ(# 9}݋GEE%,GEۜcĭkj 6@Q}Ngpp8?DaytHZ&Pی™s'G[Suwo-}ngG N:/v:/^kvq bnZ?2ki.oF8C⻫Bf~QsVNl-V-5pQ#l,~~ x$ >ս$sqW \@#lssژQCd﴿ժhmmOd(?RiV:_ߦfťIx̙YW2IO2oM;%4> ƶ:]w(9Z{4Vطub@#)J*prGJ>#KJ$ ێוUdW}c Dv wZ=FCQ&[}×vh& \.v9A:ҍ٩k Qj"`ge,ʠUNzg^1}~"8U[.(anLRRҥfU{rO*?dnO8dQWltKV&NL]d UN XsVceDܸoҔA8?ʞ1#xSPW~F-v;bG㎟9QC~*Rn-hMvH}j8U-B֒'3J]N0CH+У;YE9hy#Ƙ\g`*r"\Iu>=6 ;=*ITjKW}۝|9S9hV=秽SQz5Ȇ33 ~*{}}$e]( BsqקC#9I{ⴏVя%f` e>գjK#g'CoztW]{n qgڰSM98o϶+{]5MSX}Bw-!U m>i;Liϵj%穤/ffIm_ԗs˓'n?dKٮӱ]iڐ$Lg* nڳzh1Tqؤ`߁ދք;jH2*#N;V\H@YFzc߾)+jo28;ׯa*I]מ&_-?"Is"!>p >:2F88yi+Ysii&X3n^}xQrpGSTE;4SV[W!FZn[)_Zw/}9.2H~qR"ĪI{hPӢDu=}҂x$ ̸&e?@;9RizbO*_jr_qY4~dPd/zXYnpйV'׏n:65?u [Yd8@+W$Ec%< O1#lcRc~U:"IJ./̣ۊOx0%ȐqV3˞K_sW 2zw5 IbNs}O;OM6S\+w *rQQNx.۴3<?t>e|1\cs9Fח6!8]*PTמi(_bg+ efl虊[(T*@޾r5$-2?>+sxW@cv5 N, O$pO>ӄrbmZ_$L+}7;2ؙJŹ3O_jR `INx}t`F@>դgتڑېpiYsJ%̷@G,9?*ڊm̵ICQpB?Sac;n/h9>\0d%N{V2jت zV|,H"rbcQ d9UGve++_p-䞠*XYqǷJ ^G?zTD+6MrFc6O_zY̘rFu9;Up!?[P\ ;~etI m);QZ yś'G;r|?-GK^hq~U}jDs?ʥ_hu UcG` ֩峖Xqx# *#ۯJb˖;RVM bF 1?^ΐňPⳒ咒ȲvO )4.K~uwA>8_?/5I)7&ǂsb5 f%8!zRYM[H8T UBsMz"!X?z`Ӕ¶AW%ymI"5G2ڬD 'N{dWiDǻفBI'vT3ۓڳNW"a q:sYVlSy }MrrjNr{}+@:2F?U],7r[i Bz-٦PrGZSVAψ7_kẅH4aK2c*%%?7_7n<ŋLԈ00{?3{]ʹVboA7dl_ҟbkD.U:NҠ8g$MZ4~m +uaT"Sw'Рl2;OVe distƄ '[ Pz(f&T[ԙ3UfG8ʞi_&D#:Π%qo UI_D8Tt^\*qE34%:F^_6 c_ao!RYm@}:]/OҲR m p=*L`^+Fɉ;_e#0GLGylF*vME!]C6Uḅ=3MI݉rKg (g3qZZ`&rVi_K?bTYxDg?V6U̅֬g9:R},Q[L˵A%F"[bZ;甶'ߕX9=}E5Hr>=O3HT%ARс^Z$yAFuTr}q'>Ծ` #;PҲi_qI^ĿjWCQ&Q_ja0cg^՜gg 'è[yH!$SQrO#<]jQWZ뱖c%L M4Đ dz渦꿯SK&,Ktc!dknjW,`GnkV~ȏ愒i 9]I;~U4J*VCQ*"S1Jr{G;ՙvYvmTɪ3.0GN){_L5&@Ho,YJWvI+2>d9աRc ɷkC-?SJ➷nv?7Z5ؕ< ƺ#.o_d[~U uQ|i/h. %ks,/<2I$ry?f_ 6jvjar"dYn:~B'XNq)G!X!#8VxTQy,H#sԆv^ JF4sg 8l::>k 3rxҡn*\BRw9dsV6e?,UZQh=?Ҕe*lxu8AW!mw@q՚mh}{N }9))@W+\Dh:vڨ}8Rm/\x?*`J>$old{!r`ҋh*ÒF8:T*MP\[뎵R+]Prxua%MVtwp?:iJ8m*46ۇ@ Sqs1NH5u#gCVtRd`H.3Rm*Ro-A=-p$K獾h[\K3H=sM4ֿy6{s&K _jRYTrtϭ 7d]l\{U9%Aaϵ(SmKM?_K0L,/K&0qϨmZy黒_jq\KGԞfx[K=K2BC.⻫@#8Civykw45vTe Gۜ2'+nD3nc`\LR9eo$C#+dq_1e@!T@x)uTԬ(9ߟA[.yEԷ'+t|m, I}Y2#Y$ImRIa2J8b^!"s+%f" ;d31Zz\G+g{ko&E&TR8$(Y]q^D|KO+VQyZBwm$*=5o[]v!p5 ϯ9NPJJY29z $0I\fxgؖ1  5M8,q Sեo;/quu4S2 .X1ϵ$!J _tnUxۚXfĶOoRU/xN[iy /iڝ ̒G'9[2ee~3?yT6ҕ9s_ JHc BIvvQtvMIlA*)F9=KS!oHɾ] cG? =;*4S)_[:cQ5I}oF,y#qJ4Ӹ GE{l*TB7d)NII_6T#g8?VŤx= PDJ gF=p¬\/#qQʓ*l6hU.3}:*XcSnt WpC'c+~[k-OB u4!3 w?JA<sR_@v+8{;a!N tqdsi]N2hI]p9#Nv#P߻m?!?{E~19\'l/' `#ן)&?ӶA*×^r"JnaXzKEnk*I֨lwrj+@^ŘclہY vRM4Ȩb򷜀ݽsJz٣v)U/(5.j\XcWI$?/n}M\ j0=j?hw#]|)p8=xNA2rgRk"I{F81Ub;gzty پo.+D0d yo$`_jIޅyu%l"9RN=yŃ(f\q߾*d_ByZ9?ja{kڜ:jE1"v}}8)r񨌔濭 ˝c5A<\3Eur9t57mz9A$ʓ,F$),(˜vy(>"giL`G=Wnif)\=H㎽}M$^].E=VEߎ0Ҳ`B=GӊѧmSrx0gnj^̟$5ב8%ێ<]UBPܺve8;qM"a{VV hĻFJ͌0_gt20~Ur;Ubm ?JrF Y`c?50sAیuicOZԿVpW=Thܒ=}9ϵ%Ӽ*hiW$ds^!dZ+mP:cSiS.m'S 1^ʞw3)_܎9vA=Ԧ,aNN=T4[2}$ld*sgԢt)FhXɏ1 qdpFhj]՝.BV)QJP{^m58 1ʠ6;* -&ayi3pOAz݂vW؅aT(m{~m-̔Dù݀Upԯ?o-{BHYA+o|sT whJꡉP3v?JPTiYE:ʙ3> *i{Y[uuV=zKo?`i7J2x8ҝ 9R.k:拕[K9Yiہ۟nzL+_jF[~Gdj%H ӯ"PC|Ӝ).+K -6EP:]NOLتhg}tKV}æڽ^I b&b=CIKE_RC&pz}}ey u$]FlS 'lsN);n_qsN,[ zq_Hr2DBzo},J`Gڜͻlu&M]% *NVD&F(r9 r9&fRӐ̸\/oU&ImbAeck>) #\)*SsOJp:WKJRؔԯnXLW^U[{ ՚NP}̭ck!Jm=n6!pH8zW5oimƒ93|Gl7 ,9%>84;J9{rzTr+FI1a;~bP\Q\̖8\0/x_iX7BHxV.QE5d"%C_jV6.zԤIq)ʛIl0IԭzDKڒuÆQ=}KI"-#?ҔT= ӯ<\!%aܮXw\d|1s~OSr7*4 c犆뼩tUrbc)n^YEUR{I8ߕ_ Seq֡bXylQw#Oȓ}UlJjߗSr@Pmj\k!ea91Sį_V_d?Jֶ7aq1%s;Vjoy%vLdxDy$^+) B2OCnQe}Ҝ4'BKg}E\2JU*==<;YQFm9'+o*yg֢jV5q"U;T(@$@?(ӺC 3* e~^{Tl@뷿>)6LIYRq?w=p# zzW\ַo?ifFCN{W)5+NJSN1iwk@qLdJcxz8_v&OCӑ?J.|:#xw+ 6=}v=WzZMÛNbS##sIxݐF:F֝Fd[:B;_|0@TZ]5݀1 qD 'jIԔy~11~#62מIɴ7g~C VLxS SNJ"33cҙ.Y lS/%oM )uϵLc?}*y_&bl`AI:\sN7SlsUڬVwjo!Tsqۯj4_1 F}}m'4IU*C3Z0J`LR)34`Cm# 򩑕C1JO{zHRw^V&U83ʒkborͧ-1RU8Y^Pyc?([ЏdFJk #$ڜwq߽g%]GM ctxjc\c;~T\ZIhВ @7VT ?v9h$$aOZF*+ l* A8UUaV"^r%O'QBxm`9ZF3m+~ԯik$O_UK*)&EjQ;G~k:^tPM*fv("iW#`3ҹe*l j֜zG ÓCr<*믾 xs]d\{sNRɌi{k^Ԗ->syQ8&oO&֭?t4>1B ߕAxOൽrҶԧg/ IG|= M"kӯ$.뎟TJMߥ]*Ur֬=Ew 9`;=}i]UAaVJgv'T'r fӦ*>HyH, pBΝ}@$!@$Gڵq?!8; o3pqP@T뎿)7̹ޝ;6쐪lc9Sye4;{pq/#^2֠fjv #sϵiQ/d)5W j}Jr3I'w~-orRe6m}}R8sդV˰.7eE y SI0#9PӎZ{čnJ*;_jDK;&ؙ2uk/ Heq?&#dsx?|qދhwͿ9/,R>YU:D.=?ʭEq'̬3wm&;g*I2#ҲP|Yb*K[?!Hɑ+H%Q5tΎZۙp\7Ry\NEWHC(Ռz]=Nds?ҹmCx-.- }ӃTF%4w!Wo34/֕XCFF\'F8~֜ڒc*.z<PGotʭx~KG*]{vo9Zqwv: cƻVXʔT~[뾍F3m6֢D#XzMO:w=VQ]Vۄtɼ?4KkmWUBV,"H"/QKDCnĢk= 1UO xM-I:q[JG#mTnx{wOov3tPFq|XehWvy:{J9ZQFo7ԺwuQwog/u9:nש%u}H/9,[{^9M:ڼ z7i>_TGy3xoQp*>!b]FܕGVUԑ~5s($i<#PޜtNvZ`w6`.HqޑG"'vx0қ\o{ͥqBF;r j65O-FPwscjHh5.E_-wsS%[tuv3o&sgrN{V Է2͸sCQ'8nV8n}Kq3ռpz[y!i w׀9&9G{mrFTdaQ}yL9$dScUfc=6vOg9)'=p7t4Oǃ({;9:uy;ɭCojir~lG6}Ei57oO6:RNQeW6f^*Ud6Io` XY錞}EA _jRmDآ8$rw?}qk}tB5mR0qh`fމh6ב-αr9 1QK/׎(^\_p;oy{]oq` lsJPoF'IIԀ`=Oc}#xF=_nWԙ.Ŏy+l4k|Xgl|I9IJ1MbMn[:1ަ#sM7{ܛ}6"nz\\[rV]ѬkGHr>V&[oR[;hJt~8JFIT-hp_=WcJ_K]>9l|^'购uO|3ӯ$}LJIn1j_~|:dIKZ;_MɹBOO f rCVfꎿ *#o3'Ssqclɸ/fVӍc;?ҺgN1Ro )BkSVn% Taմi*t;iՊSk|U;OO;T+6zuWhjTK̛"_.WI~6ю=gc]\`gLP\2~ Pм?UCZo^ lFǚJ.: 4^5m-PK." ]@RCz$[aYI8XH'[IY x)Z/]dwOxOIKy60hBKxAzʒy.K'm┸[Ym 5HE).In`_<@.4,4RrP\'BzBj'{xa=V$]'5-ϔ {n|_֥T*fFA|%Sa\Fbv1n'WJ7] 4Q:mxY3_XSZU Y3<application/smilSMIL.TXT"SMIL.TXTpython-messaging-0.5.12/messaging/test/mms-data/SonyEricssonT310-R201.mms0000664000175000017500000002220111707534031025446 0ustar achiangachiang1-8db@YP55225/TYPE=PLMNapplication/smil#| Tony.gif"Tony.gifGIF89aePw1!Software: Microsoft Office!,dP3f3333f333ff3fffff3f3f̙3f3333f3333333333f3333333f3f33ff3f3f3f3333f3333333f3̙333333f333ff3ffffff3f33f3ff3f3f3ffff3fffffffffff3fffffff3fff̙ffff3fffff3f̙3333f33̙3ff3ffff̙f3f̙3f̙̙3f̙3f3333f333ff3fffff̙̙3̙f̙̙̙3f̙3f3f3333f333ff3fffff3f3f̙3f H*\ȰÇ#JHŋF ~H%3\ɲ˗0cDxr`M7ܙgO?yJѣH % )ӧ(Fj3իXjUԪ_}:6ֳ-Uh۷nȻخ݋'kmŪ\R-nUq+<5ǘ3k s͠[ l*tf1^-ZկY-Hk״qm5V%_ =+sΏ?wޜӣ_ |q{ ԯP~ˏOߊ󿅇 ,X1_zW`\ H  `tmPj-܀`x1|&AK%xb0( ^|1 ^̀3 phfQByQe|!^T 4̐ Zr%Y% T֘ #^9C `xFd{l5e5H-d㕈b!86z(4$4X 8*R{CI} ESVXxhltW k@:  ,)Hiv('xL'trn0..[na,W$ΰ^ ɉnf 5~Exa'tງzllǽ~1$s>pl @C0 3Y);B4 3?ݬL]ltSO3@;@sra}}Tt,v`q?H?G3 y65q_ol Rǩӵ rj: 96WNzبǖ lF?6: {nEICgvz5|Nk[K D;P7+/g/:NN 69r+BX޼GJ5Wp><&cc6Ѯx:2%ΑӰL <Of y.|D\8Yf#$Nԉ^6MOtZG l#^hGX7  :l`88V:SP6pGb:`}RPIuΣ5 ¶9dkzfCEQ=u)}%^"'ŵhJ!L>ȏ^- L*!`M;ҙT*Pg~T^KIJrX$HEY NKB0}gN6ɲdzP1Y\:I4>Jcmms.txtHej hopp<^audio/midiOldhPhone.mid"OldhPhone.midMThdMTrkHXQ 6YDEFAULTpp \sXi\\@\t\@XX@Xc\@X@\\aX@\@X\@XqX@\tX@\XgX\@\@X@X@\b\Xe\@\@XX@\uX@\Xa\@\@XX@\oX@Xj\@\X@\tX\@Xm\@X@\X@\g\@XkX\@X@X@\\b\@XXk\@\mX@X@\\@Xf\@X\sX@X@Xp\\@\@\uX@XXa\@X@\X@\r\@XXi\@X@\\bX@\@X\@XlX@\tX@\XqX\@\@X@\iX@\\@Xr\@X\qX@X@\Xp\@\@XX@\lX@Xl\@\\cX@X\@\@XdX@\\qX@\@XsX\@X@X@\e\\@X\@XcX@\nX@\Xk\@\@XX@\gX@Xq\\@\@X@XX@Z\sXi\\@\t\@XX@Xc\@X@\\aX@\@X\@XqX@\tX@\XgX\@\@X@X@\b\Xe\@\@XX@\uX@\Xa\@\@XX@\oX@Xj\@\X@\tX\@Xm\@X@\X@\g\@XkX\@X@X@\\b\@XXk\@\mX@X@\\@Xf\@X\sX@X@Xp\\@\@\uX@XXa\@X@\X@\r\@XXi\@X@\\bX@\@X\@XlX@\tX@\XqX\@\@X@\iX@\\@Xr\@X\qX@X@\Xp\@\@XX@\lX@Xl\@\\cX@X\@\@XdX@\\qX@\@XsX\@X@X@\e\\@X\@XcX@\nX@\Xk\@\@XX@\gX@Xq\\@\@X@XX@Z\sXi\\@\t\@XX@Xc\@X@\\aX@\@X\@XqX@\tX@\XgX\@\@X@X@\b\Xe\@\@XX@\uX@\Xa\@\@XX@\oX@Xj\@\X@\tX\@Xm\@X@\X@\g\@XkX\@X@X@\\b\@XXk\@\mX@X@\\@Xf\@X\sX@X@Xp\\@\@\uX@XXa\@X@\X@\r\@XXi\@X@\\bX@\@X\@XlX@\tX@\XqX\@\@X@\iX@\\@Xr\@X\qX@X@\Xp\@\@XX@\lX@Xl\@\\cX@X\@\@XdX@\\qX@\@XsX\@X@X@\e\\@X\@XcX@\nX@\Xk\@\@XX@\gX@Xq\\@\@X@XX@Z\sXi\\@\t\@XX@Xc\@X@\\aX@\@X\@XqX@\tX@\XgX\@\@X@X@\b\Xe\@\@XX@\uX@\Xa\@\@XX@\oX@Xj\@\X@\tX\@Xm\@X@\X@\g\@XkX\@X@X@\\b\@XXk\@\mX@X@\\@Xf\@X\sX@X@Xp\\@\@\uX@XXa\@X@\X@\r\@XXi\@X@\\bX@\@X\@XlX@\tX@\XqX\@\@X@\iX@\\@Xr\@X\qX@X@\Xp\@\@XX@\lX@Xl\@\\cX@X\@\@XdX@\\qX@\@XsX\@X@X@\e\\@X\@XcX@\nX@\Xk\@\@XX@\gX@Xq\\@\@X@XX@Z\sXi\\@\t\@XX@Xc\@X@\\aX@\@X\@XqX@\tX@\XgX\@\@X@X@\b\Xe\@\@XX@\uX@\Xa\@\@XX@\oX@Xj\@\X@\tX\@Xm\@X@\X@\g\@XkX\@X@X@\\b\@XXk\@\mX@X@\\@Xf\@X\sX@X@Xp\\@\@\uX@XXa\@X@\X@\r\@XXi\@X@\\bX@\@X\@XlX@\tX@\XqX\@\@X@\iX@\\@Xr\@X\qX@X@\Xp\@\@XX@\lX@Xl\@\\cX@X\@\@XdX@\\qX@\@XsX\@X@X@\e\\@X\@XcX@\nX@\Xk\@\@XX@\gX@Xq\\@\@X@XX@Z\sXi\\@\t\@XX@Xc\@X@\\aX@\@X\@XqX@\tX@\XgX\@\@X@X@\b\Xe\@\@XX@\uX@\Xa\@\@XX@\oX@Xj\@\X@\tX\@Xm\@X@\X@\g\@XkX\@X@X@\\b\@XXk\@\mX@X@\\@Xf\@X\sX@X@Xp\\@\@\uX@XXa\@X@\X@\r\@XXi\@X@\\bX@\@X\@XlX@\tX@\XqX\@\@X@\iX@\\@Xr\@X\qX@X@\Xp\@\@XX@\lX@Xl\@\\cX@X\@\@XdX@\\qX@\@XsX\@X@X@\e\\@X\@XcX@\nX@\Xk\@\@XX@\gX@Xq\\@\@X@XX@Z\sXi\\@\t\@XX@Xc\@X@\\aX@\@X\@XqX@\tX@\XgX\@\@X@X@\b\Xe\@\@XX@\uX@\Xa\@\@XX@\oX@Xj\@\X@\tX\@Xm\@X@\X@\g\@XkX\@X@X@\\b\@XXk\@\mX@X@\\@Xf\@X\sX@X@Xp\\@\@\uX@XXa\@X@\X@\r\@XXi\@X@\\bX@\@X\@XlX@\tX@\XqX\@\@X@\iX@\\@Xr\@X\qX@X@\Xp\@\@XX@\lX@Xl\@\\cX@X\@\@XdX@\\qX@\@XsX\@X@X@\e\\@X\@XcX@\nX@\Xk\@\@XX@\gX@Xq\\@\@X@XX@Z\sXi\\@\t\@XX@Xc\@X@\\aX@\@X\@XqX@\tX@\XgX\@\@X@X@\b\Xe\@\@XX@\uX@\Xa\@\@XX@\oX@Xj\@\X@\tX\@Xm\@X@\X@\g\@XkX\@X@X@\\b\@XXk\@\mX@X@\\@Xf\@X\sX@X@Xp\\@\@\uX@XXa\@X@\X@\r\@XXi\@X@\\bX@\@X\@XlX@\tX@\XqX\@\@X@\iX@\\@Xr\@X\qX@X@\Xp\@\@XX@\lX@Xl\@\\cX@X\@\@XdX@\\qX@\@XsX\@X@X@\e\\@X\@XcX@\nX@\Xk\@\@XX@\gX@Xq\\@\@X@XX@/'application/smilmms.smil" python-messaging-0.5.12/messaging/test/mms-data/SEC-SGHS300M.mms0000664000175000017500000000013011707534031023527 0ustar achiangachiang318870738345664/TYPE=PLMNIL"1259430.txt1259430.txtHVpython-messaging-0.5.12/messaging/test/mms-data/27d0a048cd79555de05283a22372b0eb.mms0000664000175000017500000000136611707534031026233 0ustar achiangachiang3-31cb@123/TYPE=PLMNAngående art-tillhörighetapplication/smil& Rain.wbmp"Rain.wbmp ?Ǐ?????Ϗ?fffffgfg" mms.txt"mms.txtJonatan är en gnu.'{application/smilmms.smil" python-messaging-0.5.12/messaging/test/__init__.py0000664000175000017500000000000011707534031021677 0ustar achiangachiangpython-messaging-0.5.12/messaging/test/test_wap.py0000664000175000017500000001620111707534031022000 0ustar achiangachiang# -*- coding: utf-8 -*- from array import array import unittest from messaging.sms import SmsDeliver from messaging.sms.wap import (is_a_wap_push_notification as is_push, is_mms_notification, extract_push_notification) def list_to_str(l): a = array("B", l) return a.tostring() class TestSmsWapPush(unittest.TestCase): data = [1, 6, 34, 97, 112, 112, 108, 105, 99, 97, 116, 105, 111, 110, 47, 118, 110, 100, 46, 119, 97, 112, 46, 109, 109, 115, 45, 109, 101, 115, 115, 97, 103, 101, 0, 175, 132, 140, 130, 152, 78, 79, 75, 53, 67, 105, 75, 99, 111, 84, 77, 89, 83, 71, 52, 77, 66, 83, 119, 65, 65, 115, 75, 118, 49, 52, 70, 85, 72, 65, 65, 65, 65, 65, 65, 65, 65, 0, 141, 144, 137, 25, 128, 43, 52, 52, 55, 55, 56, 53, 51, 52, 50, 55, 52, 57, 47, 84, 89, 80, 69, 61, 80, 76, 77, 78, 0, 138, 128, 142, 2, 116, 0, 136, 5, 129, 3, 1, 25, 64, 131, 104, 116, 116, 112, 58, 47, 47, 112, 114, 111, 109, 109, 115, 47, 115, 101, 114, 118, 108, 101, 116, 115, 47, 78, 79, 75, 53, 67, 105, 75, 99, 111, 84, 77, 89, 83, 71, 52, 77, 66, 83, 119, 65, 65, 115, 75, 118, 49, 52, 70, 85, 72, 65, 65, 65, 65, 65, 65, 65, 65, 0] def test_is_a_wap_push_notification(self): self.assertTrue(is_push(list_to_str(self.data))) self.assertTrue(is_push(list_to_str([1, 6, 57, 92, 45]))) self.assertFalse(is_push(list_to_str([4, 5, 57, 92, 45]))) self.assertRaises(TypeError, is_push, 1) def test_decoding_m_notification_ind(self): pdus = [ "0791447758100650400E80885810000000810004016082415464408C0C08049F8E020105040B8423F00106226170706C69636174696F6E2F766E642E7761702E6D6D732D6D65737361676500AF848C82984E4F4B3543694B636F544D595347344D4253774141734B7631344655484141414141414141008D908919802B3434373738353334323734392F545950453D504C4D4E008A808E0274008805810301194083687474703A2F", "0791447758100650440E8088581000000081000401608241547440440C08049F8E020205040B8423F02F70726F6D6D732F736572766C6574732F4E4F4B3543694B636F544D595347344D4253774141734B763134465548414141414141414100", ] number = '3838383530313030303030303138'.decode('hex') csca = "+447785016005" data = "" sms = SmsDeliver(pdus[0]) self.assertEqual(sms.udh.concat.ref, 40846) self.assertEqual(sms.udh.concat.cnt, 2) self.assertEqual(sms.udh.concat.seq, 1) self.assertEqual(sms.number, number) self.assertEqual(sms.csca, csca) data += sms.text sms = SmsDeliver(pdus[1]) self.assertEqual(sms.udh.concat.ref, 40846) self.assertEqual(sms.udh.concat.cnt, 2) self.assertEqual(sms.udh.concat.seq, 2) self.assertEqual(sms.number, number) data += sms.text mms = extract_push_notification(data) self.assertEqual(is_mms_notification(mms), True) self.assertEqual(mms.headers['Message-Type'], 'm-notification-ind') self.assertEqual(mms.headers['Transaction-Id'], 'NOK5CiKcoTMYSG4MBSwAAsKv14FUHAAAAAAAA') self.assertEqual(mms.headers['MMS-Version'], '1.0') self.assertEqual(mms.headers['From'], '2b3434373738353334323734392f545950453d504c4d4e'.decode('hex')) self.assertEqual(mms.headers['Message-Class'], 'Personal') self.assertEqual(mms.headers['Message-Size'], 29696) self.assertEqual(mms.headers['Expiry'], 72000) self.assertEqual(mms.headers['Content-Location'], 'http://promms/servlets/NOK5CiKcoTMYSG4MBSwAAsKv14FUHAAAAAAAA') pdus = [ "0791447758100650400E80885810000000800004017002314303408C0C0804DFD3020105040B8423F00106226170706C69636174696F6E2F766E642E7761702E6D6D732D6D65737361676500AF848C82984E4F4B3541315A6446544D595347344F3356514141734A763934476F4E4141414141414141008D908919802B3434373731373237353034392F545950453D504C4D4E008A808E0274008805810303F47F83687474703A2F", "0791447758100650440E8088581000000080000401700231431340440C0804DFD3020205040B8423F02F70726F6D6D732F736572766C6574732F4E4F4B3541315A6446544D595347344F3356514141734A763934476F4E414141414141414100", ] number = "88850100000008" data = "" sms = SmsDeliver(pdus[0]) self.assertEqual(sms.udh.concat.ref, 57299) self.assertEqual(sms.udh.concat.cnt, 2) self.assertEqual(sms.udh.concat.seq, 1) self.assertEqual(sms.number, number) data += sms.text sms = SmsDeliver(pdus[1]) self.assertEqual(sms.udh.concat.ref, 57299) self.assertEqual(sms.udh.concat.cnt, 2) self.assertEqual(sms.udh.concat.seq, 2) self.assertEqual(sms.number, number) data += sms.text mms = extract_push_notification(data) self.assertEqual(is_mms_notification(mms), True) self.assertEqual(mms.headers['Message-Type'], 'm-notification-ind') self.assertEqual(mms.headers['Transaction-Id'], 'NOK5A1ZdFTMYSG4O3VQAAsJv94GoNAAAAAAAA') self.assertEqual(mms.headers['MMS-Version'], '1.0') self.assertEqual(mms.headers['From'], '2b3434373731373237353034392f545950453d504c4d4e'.decode('hex')) self.assertEqual(mms.headers['Message-Class'], 'Personal') self.assertEqual(mms.headers['Message-Size'], 29696) self.assertEqual(mms.headers['Expiry'], 259199) self.assertEqual(mms.headers['Content-Location'], 'http://promms/servlets/NOK5A1ZdFTMYSG4O3VQAAsJv94GoNAAAAAAAA') def test_decoding_generic_wap_push(self): pdus = [ "0791947122725014440C8500947122921105F5112042519582408C0B05040B8423F0000396020101060B03AE81EAC3958D01A2B48403056A0A20566F6461666F6E650045C60C037761702E6D65696E63616C6C79612E64652F000801035A756D206B6F7374656E6C6F73656E20506F7274616C20224D65696E0083000322202D2065696E66616368206175662064656E20666F6C67656E64656E204C696E6B206B6C69636B656E", "0791947122725014440C8500947122921105F5112042519592403C0B05040B8423F00003960202206F6465722064696520536569746520646972656B7420617566727566656E2E2049687200830003205465616D000101", ] number = '303034393137323232393131'.decode('hex') csca = "+491722270541" data = "" sms = SmsDeliver(pdus[0]) self.assertEqual(sms.udh.concat.ref, 150) self.assertEqual(sms.udh.concat.cnt, 2) self.assertEqual(sms.udh.concat.seq, 1) self.assertEqual(sms.number, number) self.assertEqual(sms.csca, csca) data += sms.text sms = SmsDeliver(pdus[1]) self.assertEqual(sms.udh.concat.ref, 150) self.assertEqual(sms.udh.concat.cnt, 2) self.assertEqual(sms.udh.concat.seq, 2) self.assertEqual(sms.number, number) data += sms.text self.assertEqual(data, '\x01\x06\x0b\x03\xae\x81\xea\xc3\x95\x8d\x01\xa2\xb4\x84\x03\x05j\n Vodafone\x00E\xc6\x0c\x03wap.meincallya.de/\x00\x08\x01\x03Zum kostenlosen Portal "Mein\x00\x83\x00\x03" - einfach auf den folgenden Link klicken oder die Seite direkt aufrufen. Ihr\x00\x83\x00\x03 Team\x00\x01\x01') push = extract_push_notification(data) self.assertEqual(is_mms_notification(push), False) python-messaging-0.5.12/messaging/test/test_gsm_encoding.py0000664000175000017500000002012611707534031023646 0ustar achiangachiang# -*- coding: utf-8 -*- # Copyright (C) 2011 Sphere Systems Ltd # Author: Andrew Bird # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """Unittests for the gsm encoding/decoding module""" import unittest import messaging.sms.gsm0338 # imports GSM7 codec # Reversed from: ftp://ftp.unicode.org/Public/MAPPINGS/ETSI/GSM0338.TXT MAP = { # unichr(0x0000): (0x0000, 0x00), # Null u'@': (0x0040, 0x00), u'£': (0x00a3, 0x01), u'$': (0x0024, 0x02), u'¥': (0x00a5, 0x03), u'è': (0x00e8, 0x04), u'é': (0x00e9, 0x05), u'ù': (0x00f9, 0x06), u'ì': (0x00ec, 0x07), u'ò': (0x00f2, 0x08), u'Ç': (0x00c7, 0x09), # LATIN CAPITAL LETTER C WITH CEDILLA unichr(0x000a): (0x000a, 0x0a), # Linefeed u'Ø': (0x00d8, 0x0b), u'ø': (0x00f8, 0x0c), unichr(0x000d): (0x000d, 0x0d), # Carriage return u'Å': (0x00c5, 0x0e), u'å': (0x00e5, 0x0f), u'Δ': (0x0394, 0x10), u'_': (0x005f, 0x11), u'Φ': (0x03a6, 0x12), u'Γ': (0x0393, 0x13), u'Λ': (0x039b, 0x14), u'Ω': (0x03a9, 0x15), u'Π': (0x03a0, 0x16), u'Ψ': (0x03a8, 0x17), u'Σ': (0x03a3, 0x18), u'Θ': (0x0398, 0x19), u'Ξ': (0x039e, 0x1a), unichr(0x00a0): (0x00a0, 0x1b), # Escape to extension table (displayed # as NBSP, on decode of invalid escape # sequence) u'Æ': (0x00c6, 0x1c), u'æ': (0x00e6, 0x1d), u'ß': (0x00df, 0x1e), u'É': (0x00c9, 0x1f), u' ': (0x0020, 0x20), u'!': (0x0021, 0x21), u'"': (0x0022, 0x22), u'#': (0x0023, 0x23), u'¤': (0x00a4, 0x24), u'%': (0x0025, 0x25), u'&': (0x0026, 0x26), u'\'': (0x0027, 0x27), u'{': (0x007b, 0x1b28), u'}': (0x007d, 0x1b29), u'*': (0x002a, 0x2a), u'+': (0x002b, 0x2b), u',': (0x002c, 0x2c), u'-': (0x002d, 0x2d), u'.': (0x002e, 0x2e), u'\\': (0x005c, 0x1b2f), u'0': (0x0030, 0x30), u'1': (0x0031, 0x31), u'2': (0x0032, 0x32), u'3': (0x0033, 0x33), u'4': (0x0034, 0x34), u'5': (0x0035, 0x35), u'6': (0x0036, 0x36), u'7': (0x0037, 0x37), u'8': (0x0038, 0x38), u'9': (0x0039, 0x39), u':': (0x003a, 0x3a), u';': (0x003b, 0x3b), u'[': (0x005b, 0x1b3c), unichr(0x000c): (0x000c, 0x1b0a), # Formfeed u']': (0x005d, 0x1b3e), u'?': (0x003f, 0x3f), u'|': (0x007c, 0x1b40), u'A': (0x0041, 0x41), u'B': (0x0042, 0x42), u'C': (0x0043, 0x43), u'D': (0x0044, 0x44), u'E': (0x0045, 0x45), u'F': (0x0046, 0x46), u'G': (0x0047, 0x47), u'H': (0x0048, 0x48), u'I': (0x0049, 0x49), u'J': (0x004a, 0x4a), u'K': (0x004b, 0x4b), u'L': (0x004c, 0x4c), u'M': (0x004d, 0x4d), u'N': (0x004e, 0x4e), u'O': (0x004f, 0x4f), u'P': (0x0050, 0x50), u'Q': (0x0051, 0x51), u'R': (0x0052, 0x52), u'S': (0x0053, 0x53), u'T': (0x0054, 0x54), u'U': (0x0055, 0x55), u'V': (0x0056, 0x56), u'W': (0x0057, 0x57), u'X': (0x0058, 0x58), u'Y': (0x0059, 0x59), u'Z': (0x005a, 0x5a), u'Ä': (0x00c4, 0x5b), u'Ö': (0x00d6, 0x5c), u'Ñ': (0x00d1, 0x5d), u'Ü': (0x00dc, 0x5e), u'§': (0x00a7, 0x5f), u'¿': (0x00bf, 0x60), u'a': (0x0061, 0x61), u'b': (0x0062, 0x62), u'c': (0x0063, 0x63), u'd': (0x0064, 0x64), u'€': (0x20ac, 0x1b65), u'f': (0x0066, 0x66), u'g': (0x0067, 0x67), u'h': (0x0068, 0x68), u'<': (0x003c, 0x3c), u'j': (0x006a, 0x6a), u'k': (0x006b, 0x6b), u'l': (0x006c, 0x6c), u'm': (0x006d, 0x6d), u'n': (0x006e, 0x6e), u'~': (0x007e, 0x1b3d), u'p': (0x0070, 0x70), u'q': (0x0071, 0x71), u'r': (0x0072, 0x72), u's': (0x0073, 0x73), u't': (0x0074, 0x74), u'>': (0x003e, 0x3e), u'v': (0x0076, 0x76), u'i': (0x0069, 0x69), u'x': (0x0078, 0x78), u'^': (0x005e, 0x1b14), u'z': (0x007a, 0x7a), u'ä': (0x00e4, 0x7b), u'ö': (0x00f6, 0x7c), u'ñ': (0x00f1, 0x7d), u'ü': (0x00fc, 0x7e), u'à': (0x00e0, 0x7f), u'¡': (0x00a1, 0x40), u'/': (0x002f, 0x2f), u'o': (0x006f, 0x6f), u'u': (0x0075, 0x75), u'w': (0x0077, 0x77), u'y': (0x0079, 0x79), u'e': (0x0065, 0x65), u'=': (0x003d, 0x3d), u'(': (0x0028, 0x28), u')': (0x0029, 0x29), } GREEK_MAP = { # Note: these might look like Latin uppercase, but they aren't u'Α': (0x0391, 0x41), u'Β': (0x0392, 0x42), u'Ε': (0x0395, 0x45), u'Η': (0x0397, 0x48), u'Ι': (0x0399, 0x49), u'Κ': (0x039a, 0x4b), u'Μ': (0x039c, 0x4d), u'Ν': (0x039d, 0x4e), u'Ο': (0x039f, 0x4f), u'Ρ': (0x03a1, 0x50), u'Τ': (0x03a4, 0x54), u'Χ': (0x03a7, 0x58), u'Υ': (0x03a5, 0x59), u'Ζ': (0x0396, 0x5a), } QUIRK_MAP = { u'ç': (0x00e7, 0x09), } BAD = -1 class TestEncodingFunctions(unittest.TestCase): def test_encoding_supported_unicode_gsm(self): for key in MAP.keys(): # Use 'ignore' so that we see the code tested, not an exception s_gsm = key.encode('gsm0338', 'ignore') if len(s_gsm) == 1: i_gsm = ord(s_gsm) elif len(s_gsm) == 2: i_gsm = (ord(s_gsm[0]) << 8) + ord(s_gsm[1]) else: i_gsm = BAD # so we see the comparison, not an exception # We shouldn't generate an invalid escape sequence if key == unichr(0x00a0): self.assertEqual(BAD, i_gsm) else: self.assertEqual(MAP[key][1], i_gsm) def test_encoding_supported_greek_unicode_gsm(self): # Note: Conversion is one way, hence no corresponding decode test for key in GREEK_MAP.keys(): # Use 'replace' so that we trigger the mapping s_gsm = key.encode('gsm0338', 'replace') if len(s_gsm) == 1: i_gsm = ord(s_gsm) else: i_gsm = BAD # so we see the comparison, not an exception self.assertEqual(GREEK_MAP[key][1], i_gsm) def test_encoding_supported_quirk_unicode_gsm(self): # Note: Conversion is one way, hence no corresponding decode test for key in QUIRK_MAP.keys(): # Use 'replace' so that we trigger the mapping s_gsm = key.encode('gsm0338', 'replace') if len(s_gsm) == 1: i_gsm = ord(s_gsm) else: i_gsm = BAD # so we see the comparison, not an exception self.assertEqual(QUIRK_MAP[key][1], i_gsm) def test_decoding_supported_unicode_gsm(self): for key in MAP.keys(): i_gsm = MAP[key][1] if i_gsm <= 0xff: s_gsm = chr(i_gsm) elif i_gsm <= 0xffff: s_gsm = chr((i_gsm & 0xff00) >> 8) s_gsm += chr(i_gsm & 0x00ff) s_unicode = s_gsm.decode('gsm0338', 'strict') self.assertEqual(MAP[key][0], ord(s_unicode)) def test_is_gsm_text_true(self): for key in MAP.keys(): if key == unichr(0x00a0): continue self.assertEqual(messaging.sms.gsm0338.is_gsm_text(key), True) def test_is_gsm_text_false(self): self.assertEqual( messaging.sms.gsm0338.is_gsm_text(unichr(0x00a0)), False) for i in xrange(1, 0xffff + 1): if unichr(i) not in MAP: # Note: it's a little odd, but on error we want to see values if messaging.sms.gsm0338.is_gsm_text(unichr(i)) is not False: self.assertEqual(BAD, i) python-messaging-0.5.12/messaging/test/test_udh.py0000664000175000017500000000134611707534031021775 0ustar achiangachiangimport unittest from messaging.sms.udh import UserDataHeader from messaging.utils import to_array class TestUserDataHeader(unittest.TestCase): def test_user_data_header(self): data = to_array("08049f8e020105040b8423f0") udh = UserDataHeader.from_bytes(data) self.assertEqual(udh.concat.seq, 1) self.assertEqual(udh.concat.cnt, 2) self.assertEqual(udh.concat.ref, 40846) self.assertEqual(udh.ports.dest_port, 2948) self.assertEqual(udh.ports.orig_port, 9200) data = to_array("0003190201") udh = UserDataHeader.from_bytes(data) self.assertEqual(udh.concat.seq, 1) self.assertEqual(udh.concat.cnt, 2) self.assertEqual(udh.concat.ref, 25) python-messaging-0.5.12/messaging/test/test_sms.py0000664000175000017500000004502311707534031022017 0ustar achiangachiang# -*- coding: utf-8 -*- from datetime import datetime, timedelta import unittest from messaging.sms import SmsSubmit, SmsDeliver from messaging.utils import (timedelta_to_relative_validity as to_relative, datetime_to_absolute_validity as to_absolute, FixedOffset) class TestEncodingFunctions(unittest.TestCase): def test_converting_timedelta_to_validity(self): self.assertEqual(to_relative(timedelta(minutes=5)), 0) self.assertEqual(to_relative(timedelta(minutes=6)), 0) self.assertEqual(to_relative(timedelta(minutes=10)), 1) self.assertEqual(to_relative(timedelta(hours=12)), 143) self.assertEqual(to_relative(timedelta(hours=13)), 145) self.assertEqual(to_relative(timedelta(hours=24)), 167) self.assertEqual(to_relative(timedelta(days=2)), 168) self.assertEqual(to_relative(timedelta(days=30)), 196) def test_converting_datetime_to_validity(self): # http://www.dreamfabric.com/sms/scts.html # 12. Feb 1999 05:57:30 GMT+3 when = datetime(1999, 2, 12, 5, 57, 30, 0, FixedOffset(3 * 60, "GMT+3")) expected = [0x99, 0x20, 0x21, 0x50, 0x75, 0x03, 0x21] self.assertEqual(to_absolute(when, "GMT+3"), expected) when = datetime(1999, 2, 12, 5, 57, 30, 0) expected = [0x99, 0x20, 0x21, 0x50, 0x75, 0x03, 0x0] self.assertEqual(to_absolute(when, "UTC"), expected) when = datetime(1999, 2, 12, 5, 57, 30, 0, FixedOffset(-3 * 60, "GMT-3")) expected = [0x99, 0x20, 0x21, 0x50, 0x75, 0x03, 0x29] self.assertEqual(to_absolute(when, "GMT-3"), expected) class TestSmsSubmit(unittest.TestCase): def test_encoding_validity(self): # no validity number = '2b3334363136353835313139'.decode('hex') text = "hola" expected = "0001000B914316565811F9000004E8373B0C" sms = SmsSubmit(number, text) sms.ref = 0x0 pdu = sms.to_pdu()[0] self.assertEqual(pdu.pdu, expected) # absolute validity number = '2b3334363136353835313139'.decode('hex') text = "hola" expected = "0019000B914316565811F900000170520251930004E8373B0C" sms = SmsSubmit(number, text) sms.ref = 0x0 sms.validity = datetime(2010, 7, 25, 20, 15, 39) pdu = sms.to_pdu()[0] self.assertEqual(pdu.pdu, expected) # relative validity number = '2b3334363136353835313139'.decode('hex') text = "hola" expected = "0011000B914316565811F90000AA04E8373B0C" expected_len = 18 sms = SmsSubmit(number, text) sms.ref = 0x0 sms.validity = timedelta(days=4) pdu = sms.to_pdu()[0] self.assertEqual(pdu.pdu, expected) self.assertEqual(pdu.length, expected_len) def test_encoding_csca(self): number = '2b3334363136353835313139'.decode('hex') text = "hola" csca = "+34646456456" expected = "07914346466554F601000B914316565811F9000004E8373B0C" expected_len = 17 sms = SmsSubmit(number, text) sms.csca = csca sms.ref = 0x0 pdu = sms.to_pdu()[0] self.assertEqual(pdu.pdu, expected) self.assertEqual(pdu.length, expected_len) self.assertEqual(pdu.cnt, 1) self.assertEqual(pdu.seq, 1) def test_encoding_class(self): number = '2b3334363534313233343536'.decode('hex') text = "hey yo" expected_0 = "0001000B914356143254F6001006E8721E947F03" expected_1 = "0001000B914356143254F6001106E8721E947F03" expected_2 = "0001000B914356143254F6001206E8721E947F03" expected_3 = "0001000B914356143254F6001306E8721E947F03" sms = SmsSubmit(number, text) sms.ref = 0x0 sms.klass = 0 pdu = sms.to_pdu()[0] self.assertEqual(pdu.pdu, expected_0) sms.klass = 1 pdu = sms.to_pdu()[0] self.assertEqual(pdu.pdu, expected_1) sms.klass = 2 pdu = sms.to_pdu()[0] self.assertEqual(pdu.pdu, expected_2) sms.klass = 3 pdu = sms.to_pdu()[0] self.assertEqual(pdu.pdu, expected_3) def test_encoding_request_status(self): # tested with pduspy.exe and http://www.rednaxela.net/pdu.php number = '2b3334363534313233343536'.decode('hex') text = "hey yo" expected = "0021000B914356143254F6000006E8721E947F03" sms = SmsSubmit(number, text) sms.ref = 0x0 sms.request_status = True pdu = sms.to_pdu()[0] self.assertEqual(pdu.pdu, expected) def test_encoding_message_with_latin1_chars(self): # tested with pduspy.exe number = '2b3334363534313233343536'.decode('hex') text = u"Hölä" expected = "0011000B914356143254F60000AA04483E7B0F" sms = SmsSubmit(number, text) sms.ref = 0x0 sms.validity = timedelta(days=4) pdu = sms.to_pdu()[0] self.assertEqual(pdu.pdu, expected) # tested with pduspy.exe number = '2b3334363534313233343536'.decode('hex') text = u"BÄRÇA äñ@" expected = "0001000B914356143254F6000009C2AD341104EDFB00" sms = SmsSubmit(number, text) sms.ref = 0x0 pdu = sms.to_pdu()[0] self.assertEqual(pdu.pdu, expected) def test_encoding_8bit_message(self): number = "01000000000" csca = "+44000000000" text = "Hi there..." expected = "07914400000000F001000B811000000000F000040B48692074686572652E2E2E" sms = SmsSubmit(number, text) sms.ref = 0x0 sms.csca = csca sms.fmt = 0x04 # 8 bits pdu = sms.to_pdu()[0] self.assertEqual(pdu.pdu, expected) def test_encoding_ucs2_message(self): number = '2b3334363136353835313139'.decode('hex') text = u'あ叶葉' csca = '+34646456456' expected = "07914346466554F601000B914316565811F9000806304253F68449" sms = SmsSubmit(number, text) sms.ref = 0x0 sms.csca = csca pdu = sms.to_pdu()[0] self.assertEqual(pdu.pdu, expected) text = u"Русский" number = '363535333435363738'.decode('hex') expected = "001100098156355476F80008AA0E0420044304410441043A04380439" sms = SmsSubmit(number, text) sms.ref = 0x0 sms.validity = timedelta(days=4) pdu = sms.to_pdu()[0] self.assertEqual(pdu.pdu, expected) def test_encoding_multipart_7bit(self): # text encoded with umts-tools text = "Or walk with Kings - nor lose the common touch, if neither foes nor loving friends can hurt you, If all men count with you, but none too much; If you can fill the unforgiving minute With sixty seconds' worth of distance run, Yours is the Earth and everything thats in it, And - which is more - you will be a Man, my son" number = '363535333435363738'.decode('hex') expected = [ "005100098156355476F80000AAA00500038803019E72D03DCC5E83EE693A1AB44CBBCF73500BE47ECB41ECF7BC0CA2A3CBA0F1BBDD7EBB41F4777D8C6681D26690BB9CA6A3CB7290F95D9E83DC6F3988FDB6A7DD6790599E2EBBC973D038EC06A1EB723A28FFAEB340493328CC6683DA653768FCAEBBE9A07B9A8E06E5DF7516485CA783DC6F7719447FBF41EDFA18BD0325CDA0FCBB0E1A87DD", "005100098156355476F80000AAA005000388030240E6349B0DA2A3CBA0BADBFC969FD3F6B4FB0C6AA7DD757A19744DD3D1A0791A4FCF83E6E5F1DB4D9E9F40F7B79C8E06BDCD20727A4E0FBBC76590BCEE6681B2EFBA7C0E4ACF41747419540CCBE96850D84D0695ED65799E8E4EBBCF203A3A4C9F83D26E509ACE0205DD64500B7447A7C768507A0E6ABFE565500B947FD741F7349B0D129741", "005100098156355476F80000AA14050003880303C2A066D8CD02B5F3A0F9DB0D", ] sms = SmsSubmit(number, text) sms.ref = 0x0 sms.rand_id = 136 sms.validity = timedelta(days=4) ret = sms.to_pdu() cnt = len(ret) for i, pdu in enumerate(ret): self.assertEqual(pdu.pdu, expected[i]) self.assertEqual(pdu.seq, i + 1) self.assertEqual(pdu.cnt, cnt) def test_encoding_bad_number_raises_error(self): self.assertRaises(ValueError, SmsSubmit, "032BADNUMBER", "text") def test_encoding_bad_csca_raises_error(self): sms = SmsSubmit("54342342", "text") self.assertRaises(ValueError, setattr, sms, 'csca', "1badcsca") class TestSubmitPduCounts(unittest.TestCase): DEST = "+3530000000" GSM_CHAR = "x" EGSM_CHAR = u"€" UNICODE_CHAR = u"ő" def test_gsm_1(self): sms = SmsSubmit(self.DEST, self.GSM_CHAR * 160) self.assertEqual(len(sms.to_pdu()), 1) def test_gsm_2(self): sms = SmsSubmit(self.DEST, self.GSM_CHAR * 161) self.assertEqual(len(sms.to_pdu()), 2) def test_gsm_3(self): sms = SmsSubmit(self.DEST, self.GSM_CHAR * 153 * 2) self.assertEqual(len(sms.to_pdu()), 2) def test_gsm_4(self): sms = SmsSubmit(self.DEST, self.GSM_CHAR * 153 * 2 + self.GSM_CHAR) self.assertEqual(len(sms.to_pdu()), 3) def test_gsm_5(self): sms = SmsSubmit(self.DEST, self.GSM_CHAR * 153 * 3) self.assertEqual(len(sms.to_pdu()), 3) def test_gsm_6(self): sms = SmsSubmit(self.DEST, self.GSM_CHAR * 153 * 3 + self.GSM_CHAR) self.assertEqual(len(sms.to_pdu()), 4) def test_egsm_1(self): sms = SmsSubmit(self.DEST, self.EGSM_CHAR * 80) self.assertEqual(len(sms.to_pdu()), 1) def test_egsm_2(self): sms = SmsSubmit(self.DEST, self.EGSM_CHAR * 79 + self.GSM_CHAR) self.assertEqual(len(sms.to_pdu()), 1) def test_egsm_3(self): sms = SmsSubmit(self.DEST, self.EGSM_CHAR * 153) # 306 septets self.assertEqual(len(sms.to_pdu()), 3) def test_egsm_4(self): sms = SmsSubmit(self.DEST, self.EGSM_CHAR * 229 + self.GSM_CHAR) # 459 septets self.assertEqual(len(sms.to_pdu()), 4) def test_unicode_1(self): sms = SmsSubmit(self.DEST, self.UNICODE_CHAR * 70) self.assertEqual(len(sms.to_pdu()), 1) def test_unicode_2(self): sms = SmsSubmit(self.DEST, self.UNICODE_CHAR * 70 + self.GSM_CHAR) self.assertEqual(len(sms.to_pdu()), 2) def test_unicode_3(self): sms = SmsSubmit(self.DEST, self.UNICODE_CHAR * 67 * 2) self.assertEqual(len(sms.to_pdu()), 2) def test_unicode_4(self): sms = SmsSubmit(self.DEST, self.UNICODE_CHAR * 67 * 2 + self.GSM_CHAR) self.assertEqual(len(sms.to_pdu()), 3) def test_unicode_5(self): sms = SmsSubmit(self.DEST, self.UNICODE_CHAR * 67 * 3) self.assertEqual(len(sms.to_pdu()), 3) def test_unicode_6(self): sms = SmsSubmit(self.DEST, self.UNICODE_CHAR * 67 * 3 + self.GSM_CHAR) self.assertEqual(len(sms.to_pdu()), 4) class TestSmsDeliver(unittest.TestCase): def test_decoding_7bit_pdu(self): pdu = "07911326040000F0040B911346610089F60000208062917314080CC8F71D14969741F977FD07" text = "How are you?" csca = "+31624000000" number = '2b3331363431363030393836'.decode('hex') sms = SmsDeliver(pdu) self.assertEqual(sms.text, text) self.assertEqual(sms.csca, csca) self.assertEqual(sms.number, number) def test_decoding_ucs2_pdu(self): pdu = "07914306073011F0040B914316709807F2000880604290224080084E2D5174901A8BAF" text = u"中兴通讯" csca = "+34607003110" number = '2b3334363130373839373032'.decode('hex') sms = SmsDeliver(pdu) self.assertEqual(sms.text, text) self.assertEqual(sms.csca, csca) self.assertEqual(sms.number, number) def test_decoding_7bit_pdu_data(self): pdu = "07911326040000F0040B911346610089F60000208062917314080CC8F71D14969741F977FD07" text = "How are you?" csca = "+31624000000" number = '2b3331363431363030393836'.decode('hex') data = SmsDeliver(pdu).data self.assertEqual(data['text'], text) self.assertEqual(data['csca'], csca) self.assertEqual(data['number'], number) self.assertEqual(data['pid'], 0) self.assertEqual(data['fmt'], 0) self.assertEqual(data['date'], datetime(2002, 8, 26, 19, 37, 41)) def test_decoding_datetime_gmtplusone(self): pdu = "0791447758100650040C914497716247010000909010711423400A2050EC468B81C4733A" text = " 1741 bst" number = '2b343437393137323637343130'.decode('hex') date = datetime(2009, 9, 1, 16, 41, 32) sms = SmsDeliver(pdu) self.assertEqual(sms.text, text) self.assertEqual(sms.number, number) self.assertEqual(sms.date, date) def test_decoding_datetime_gmtminusthree(self): pdu = "0791553001000001040491578800000190115101112979CF340B342F9FEBE536E83D0791C3E4F71C440E83E6F53068FE66A7C7697A781C7EBB4050F99BFE1EBFD96F1D48068BC16030182E66ABD560B41988FC06D1D3F03768FA66A7C7697A781C7E83CCEF34282C2ECBE96F50B90D8AC55EB0DC4B068BC140B1994E16D3D1622E" date = datetime(2010, 9, 11, 18, 10, 11) # 11/09/10 15:10 GMT-3.00 sms = SmsDeliver(pdu) self.assertEqual(sms.date, date) def test_decoding_number_alphanumeric(self): # Odd length test pdu = "07919471060040340409D0C6A733390400009060920173018093CC74595C96838C4F6772085AD6DDE4320B444E9741D4B03C6D7EC3E9E9B71B9474D3CB727799DEA286CFE5B9991DA6CBC3F432E85E9793CBA0F09A9EB6A7CB72BA0B9474D3CB727799DE72D6E9FABAFB0CBAA7E56490BA4CD7D34170F91BE4ACD3F575F7794E0F9F4161F1B92C2F8FD1EE32DD054AA2E520E3D3991C82A8E5701B" number = "FONIC" text = "Lieber FONIC Kunde, die Tarifoption Internet-Tagesflatrate wurde aktiviert. Internet-Nutzung wird jetzt pro Nutzungstag abgerechnet. Ihr FONIC Team" csca = "+491760000443" sms = SmsDeliver(pdu) self.assertEqual(sms.text, text) self.assertEqual(sms.csca, csca) self.assertEqual(sms.number, number) # Even length test pdu = "07919333852804000412D0F7FBDD454FB75D693A0000903002801153402BCD301E9F0605D9E971191483C140412A35690D52832063D2F9040599A058EE05A3BD6430580E" number = "www.tim.it" text = 'Maxxi Alice 100 ATTIVATA FINO AL 19/04/2009' csca = '+393358824000' sms = SmsDeliver(pdu) self.assertEqual(sms.text, text) self.assertEqual(sms.csca, csca) self.assertEqual(sms.number, number) def test_decode_sms_confirmation(self): pdu = "07914306073011F006270B913426565711F7012081111345400120811174054043" csca = "+34607003110" date = datetime(2010, 2, 18, 11, 31, 54) number = "SR-UNKNOWN" # XXX: the number should be +344626575117, is the prefix flipped ? text = "+43626575117|10/02/18 11:31:54|" sms = SmsDeliver(pdu) self.assertEqual(sms.text, text) self.assertEqual(sms.csca, csca) self.assertEqual(sms.number, number) self.assertEqual(sms.date, date) def test_decode_weird_multipart_german_pdu(self): pdus = [ "07919471227210244405852122F039F101506271217180A005000319020198E9B2B82C0759DFE4B0F9ED2EB7967537B9CC02B5D37450122D2FCB41EE303DFD7687D96537881A96A7CD6F383DFD7683F46134BBEC064DD36550DA0D22A7CBF3721BE42CD3F5A0198B56036DCA20B8FC0D6A0A4170767D0EAAE540433A082E7F83A6E5F93CFD76BB40D7B2DB0D9AA6CB2072BA3C2F83926EF31BE44E8FD17450BB8C9683CA", "07919471227210244405852122F039F1015062712181804F050003190202E4E8309B5E7683DAFC319A5E76B340F73D9A5D7683A6E93268FD9ED3CB6EF67B0E5AD172B19B2C2693C9602E90355D6683A6F0B007946E8382F5393BEC26BB00", ] texts = [ u"Lieber Vodafone-Kunde, mit Ihrer nationalen Tarifoption zahlen Sie in diesem Netz 3,45 € pro MB plus 59 Ct pro Session. Wenn Sie diese Info nicht mehr e", u"rhalten möchten, wählen Sie kostenlos +4917212220. Viel Spaß im Ausland.", ] for i, sms in enumerate(map(SmsDeliver, pdus)): self.assertEqual(sms.text, texts[i]) self.assertEqual(sms.udh.concat.cnt, len(pdus)) self.assertEqual(sms.udh.concat.seq, i + 1) self.assertEqual(sms.udh.concat.ref, 25) def test_decoding_odd_length_pdu_strict_raises_valueerror(self): # same pdu as in test_decoding_number_alpha1 minus last char pdu = "07919471060040340409D0C6A733390400009060920173018093CC74595C96838C4F6772085AD6DDE4320B444E9741D4B03C6D7EC3E9E9B71B9474D3CB727799DEA286CFE5B9991DA6CBC3F432E85E9793CBA0F09A9EB6A7CB72BA0B9474D3CB727799DE72D6E9FABAFB0CBAA7E56490BA4CD7D34170F91BE4ACD3F575F7794E0F9F4161F1B92C2F8FD1EE32DD054AA2E520E3D3991C82A8E5701" self.assertRaises(ValueError, SmsDeliver, pdu) def test_decoding_odd_length_pdu_no_strict(self): # same pdu as in test_decoding_number_alpha1 minus last char pdu = "07919471060040340409D0C6A733390400009060920173018093CC74595C96838C4F6772085AD6DDE4320B444E9741D4B03C6D7EC3E9E9B71B9474D3CB727799DEA286CFE5B9991DA6CBC3F432E85E9793CBA0F09A9EB6A7CB72BA0B9474D3CB727799DE72D6E9FABAFB0CBAA7E56490BA4CD7D34170F91BE4ACD3F575F7794E0F9F4161F1B92C2F8FD1EE32DD054AA2E520E3D3991C82A8E5701" text = "Lieber FONIC Kunde, die Tarifoption Internet-Tagesflatrate wurde aktiviert. Internet-Nutzung wird jetzt pro Nutzungstag abgerechnet. Ihr FONIC Tea" sms = SmsDeliver(pdu, strict=False) self.assertEqual(sms.text, text) def test_decoding_delivery_status_report(self): pdu = "0791538375000075061805810531F1019082416500400190824165004000" sr = { 'status': 0, 'scts': datetime(2010, 9, 28, 14, 56), 'dt': datetime(2010, 9, 28, 14, 56), 'recipient': '50131' } sms = SmsDeliver(pdu) self.assertEqual(sms.csca, "+353857000057") data = sms.data self.assertEqual(data['ref'], 24) self.assertEqual(sms.sr, sr) def test_decoding_delivery_status_report_without_smsc_address(self): pdu = "00060505810531F1010150610000400101506100004000" sr = { 'status': 0, 'scts': datetime(2010, 10, 5, 16, 0), 'dt': datetime(2010, 10, 5, 16, 0), 'recipient': '50131' } sms = SmsDeliver(pdu) self.assertEqual(sms.csca, None) data = sms.data self.assertEqual(data['ref'], 5) self.assertEqual(sms.sr, sr) # XXX: renable when support added # def test_decoding_submit_status_report(self): # # sent from SMSC to indicate submission failed or additional info # pdu = "07914306073011F001000B914306565711F9000007F0B2FC0DCABF01" # csca = "+34607003110" # number = "SR-UNKNOWN" # # sms = SmsDeliver(pdu) # self.assertEqual(sms.csca, csca) # self.assertEqual(sms.number, number) python-messaging-0.5.12/messaging/test/test_mms.py0000664000175000017500000006135211707534031022014 0ustar achiangachiang# -*- coding: utf-8 -*- from array import array import datetime import os import unittest from messaging.mms.message import MMSMessage # test data extracted from heyman's # http://github.com/heyman/mms-decoder DATA_DIR = os.path.join(os.path.dirname(__file__), 'mms-data') class TestMmsDecoding(unittest.TestCase): def test_decoding_from_data(self): path = os.path.join(DATA_DIR, 'iPhone.mms') data = array("B", open(path, 'rb').read()) mms = MMSMessage.from_data(data) headers = { 'From': '', 'Transaction-Id': '1262957356-3', 'MMS-Version': '1.2', 'To': '1337/TYPE=PLMN', 'Message-Type': 'm-send-req', 'Content-Type': ('application/vnd.wap.multipart.related', {'Start': '0.smil', 'Type': 'application/smil'}), } self.assertEqual(mms.headers, headers) def test_decoding_iPhone_mms(self): path = os.path.join(DATA_DIR, 'iPhone.mms') mms = MMSMessage.from_file(path) self.assertTrue(isinstance(mms, MMSMessage)) headers = { 'From': '', 'Transaction-Id': '1262957356-3', 'MMS-Version': '1.2', 'To': '1337/TYPE=PLMN', 'Message-Type': 'm-send-req', 'Content-Type': ('application/vnd.wap.multipart.related', {'Start': '0.smil', 'Type': 'application/smil'}), } smil_data = '\n\n\n \n\n\n\n\n\n\n\n\n\n\n' self.assertEqual(mms.headers, headers) self.assertEqual(mms.content_type, 'application/vnd.wap.multipart.related') self.assertEqual(len(mms.data_parts), 2) self.assertEqual(mms.data_parts[0].content_type, 'application/smil') self.assertEqual(mms.data_parts[0].data, smil_data) self.assertEqual(mms.data_parts[1].content_type, 'image/jpeg') self.assertEqual(mms.data_parts[1].content_type_parameters, {'Name': 'IMG_6807.jpg'}) def test_decoding_SIMPLE_mms(self): path = os.path.join(DATA_DIR, 'SIMPLE.MMS') mms = MMSMessage.from_file(path) self.assertTrue(isinstance(mms, MMSMessage)) headers = { 'Transaction-Id': '1234', 'MMS-Version': '1.0', 'Message-Type': 'm-retrieve-conf', 'Date': datetime.datetime(2002, 12, 20, 21, 26, 56), 'Content-Type': ('application/vnd.wap.multipart.related', {}), 'Subject': 'Simple message', } text_data = "This is a simple MMS message with a single text body part." self.assertEqual(mms.headers, headers) self.assertEqual(mms.content_type, 'application/vnd.wap.multipart.related') self.assertEqual(len(mms.data_parts), 1) self.assertEqual(mms.data_parts[0].content_type, 'text/plain') self.assertEqual(mms.data_parts[0].data, text_data) def test_decoding_BTMMS_mms(self): path = os.path.join(DATA_DIR, 'BTMMS.MMS') mms = MMSMessage.from_file(path) self.assertTrue(isinstance(mms, MMSMessage)) headers = { 'Transaction-Id': '1234', 'MMS-Version': '1.0', 'Message-Type': 'm-retrieve-conf', 'Date': datetime.datetime(2003, 1, 21, 1, 57, 4), 'Content-Type': ('application/vnd.wap.multipart.related', {'Start': '', 'Type': 'application/smil'}), 'Subject': 'BT Ignite MMS', } smil_data = '\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n' text_data = 'BT Ignite\r\n\r\nMMS Services' self.assertEqual(mms.headers, headers) self.assertEqual(mms.content_type, 'application/vnd.wap.multipart.related') self.assertEqual(len(mms.data_parts), 4) self.assertEqual(mms.data_parts[0].content_type, 'application/smil') self.assertEqual(mms.data_parts[0].data, smil_data) self.assertEqual(mms.data_parts[1].content_type, 'image/gif') self.assertEqual(mms.data_parts[2].content_type, 'audio/amr') self.assertEqual(mms.data_parts[3].content_type, 'text/plain') self.assertEqual(mms.data_parts[3].data, text_data) def test_decoding_TOMSLOT_mms(self): path = os.path.join(DATA_DIR, 'TOMSLOT.MMS') mms = MMSMessage.from_file(path) self.assertTrue(isinstance(mms, MMSMessage)) headers = { 'From': '616c6c616e40746f6d736c6f742e636f6d'.decode('hex'), 'Transaction-Id': '1234', 'MMS-Version': '1.0', 'Message-Type': 'm-retrieve-conf', 'Date': datetime.datetime(2003, 2, 16, 3, 48, 33), 'Content-Type': ('application/vnd.wap.multipart.related', {'Start': '', 'Type': 'application/smil'}), 'Subject': 'Tom Slot Band', } smil_data = '\r\n\t\r\n\t\t\r\n\t\t\r\n\t\t\r\n\t\t\t\r\n\t\t\t\r\n\t\t\t\r\n\t\t\r\n\t\r\n\t\r\n\t\t\r\n\t\t\t\r\n\t\t\r\n\t\t\r\n\t\t\t\r\n\t\t\r\n\t\t\r\n\t\t\t\r\n\t\t\r\n\t\t\r\n\t\t\t\r\n\t\t\r\n\t\t\r\n\t\t\t\r\n\t\t\t\r\n\t\t\t\t\r\n\t\t\t\r\n\t\t\t\r\n\t\r\n\r\n' text_data = 'Presented by NowMMS\r\n' self.assertEqual(mms.headers, headers) self.assertEqual(mms.content_type, 'application/vnd.wap.multipart.related') self.assertEqual(len(mms.data_parts), 8) self.assertEqual(mms.data_parts[0].content_type, 'application/smil') self.assertEqual(mms.data_parts[0].data, smil_data) self.assertEqual(mms.data_parts[1].content_type, 'image/jpeg') self.assertEqual(mms.data_parts[2].content_type, 'image/jpeg') self.assertEqual(mms.data_parts[3].content_type, 'image/jpeg') self.assertEqual(mms.data_parts[4].content_type, 'image/jpeg') self.assertEqual(mms.data_parts[5].content_type, 'image/jpeg') self.assertEqual(mms.data_parts[6].content_type, 'text/plain') self.assertEqual(mms.data_parts[6].data, text_data) self.assertEqual(mms.data_parts[7].content_type, 'audio/amr') def test_decoding_images_are_cut_off_debug_mms(self): path = os.path.join(DATA_DIR, 'images_are_cut_off_debug.mms') mms = MMSMessage.from_file(path) self.assertTrue(isinstance(mms, MMSMessage)) headers = { 'From': '', 'Read-Reply': False, 'Transaction-Id': '2112410527', 'MMS-Version': '1.0', 'To': '7464707440616a616a672e63646d'.decode('hex'), 'Delivery-Report': False, 'Message-Type': 'm-send-req', 'Content-Type': ('application/vnd.wap.multipart.related', {'Start': '', 'Type': 'application/smil'}), 'Subject': 'Picture3', } smil_data = '' self.assertEqual(mms.headers, headers) self.assertEqual(len(mms.data_parts), 2) self.assertEqual(mms.content_type, 'application/vnd.wap.multipart.related') self.assertEqual(mms.data_parts[0].content_type, 'image/jpeg') self.assertEqual(mms.data_parts[0].content_type_parameters, {'Name': 'Picture3.jpg'}) self.assertEqual(mms.data_parts[1].content_type, 'application/smil') self.assertEqual(mms.data_parts[1].data, smil_data) def test_decoding_openwave_mms(self): path = os.path.join(DATA_DIR, 'openwave.mms') mms = MMSMessage.from_file(path) self.assertTrue(isinstance(mms, MMSMessage)) headers = { 'From': '2b31363530353535303030302f545950453d504c4d4e'.decode('hex'), 'Message-Class': 'Personal', 'Transaction-Id': '1067263672', 'MMS-Version': '1.0', 'Priority': 'Normal', 'To': '112/TYPE=PLMN', 'Delivery-Report': False, 'Message-Type': 'm-send-req', 'Content-Type': ('application/vnd.wap.multipart.related', {'Start': '', 'Type': 'application/smil'}), 'Subject': 'rubrik', } smil_data = '\n \n \n \n \n \n \n \n \n \n \n \n \n\n' text_data = 'rubrik' self.assertEqual(mms.headers, headers) self.assertEqual(len(mms.data_parts), 2) self.assertEqual(mms.content_type, 'application/vnd.wap.multipart.related') self.assertEqual(mms.data_parts[0].content_type, 'application/smil') self.assertEqual(mms.data_parts[0].data, smil_data) self.assertEqual(mms.data_parts[1].data, text_data) def test_decoding_SonyEricssonT310_R201_mms(self): path = os.path.join(DATA_DIR, 'SonyEricssonT310-R201.mms') mms = MMSMessage.from_file(path) self.assertTrue(isinstance(mms, MMSMessage)) headers = { 'Sender-Visibility': 'Show', 'From': '', 'Read-Reply': False, 'Message-Class': 'Personal', 'Transaction-Id': '1-8db', 'MMS-Version': '1.0', 'Priority': 'Normal', 'To': '55225/TYPE=PLMN', 'Delivery-Report': False, 'Message-Type': 'm-send-req', 'Date': datetime.datetime(2004, 3, 18, 7, 30, 34), 'Content-Type': ('application/vnd.wap.multipart.related', {'Start': '', 'Type': 'application/smil'}), } text_data = 'Hej hopp' smil_data = '\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n' self.assertEqual(mms.headers, headers) self.assertEqual(len(mms.data_parts), 4) self.assertEqual(mms.content_type, 'application/vnd.wap.multipart.related') self.assertEqual(mms.data_parts[0].content_type, 'image/gif') self.assertEqual(mms.data_parts[0].content_type_parameters, {'Name': 'Tony.gif'}) self.assertEqual(mms.data_parts[1].content_type, 'text/plain') self.assertEqual(mms.data_parts[1].data, text_data) self.assertEqual(mms.data_parts[2].content_type, 'audio/midi') self.assertEqual(mms.data_parts[2].content_type_parameters, {'Name': 'OldhPhone.mid'}) self.assertEqual(mms.data_parts[3].content_type, 'application/smil') self.assertEqual(mms.data_parts[3].data, smil_data) def test_decoding_gallery2test_mms(self): path = os.path.join(DATA_DIR, 'gallery2test.mms') mms = MMSMessage.from_file(path) self.assertTrue(isinstance(mms, MMSMessage)) headers = { 'From': '2b31363530353535303030302f545950453d504c4d4e'.decode('hex'), 'Message-Class': 'Personal', 'Transaction-Id': '1118775337', 'MMS-Version': '1.0', 'Priority': 'Normal', 'To': 'Jg', 'Delivery-Report': False, 'Message-Type': 'm-send-req', 'Content-Type': ('application/vnd.wap.multipart.related', {'Start': '', 'Type': 'application/smil'}), 'Subject': 'Jgj', } text_data = 'Jgj' smil_data = '\n \n \n \n \n \n \n \n \n \n gnu-head\n \n \n \n\n' self.assertEqual(mms.headers, headers) self.assertEqual(len(mms.data_parts), 3) self.assertEqual(mms.content_type, 'application/vnd.wap.multipart.related') self.assertEqual(mms.data_parts[0].content_type, 'application/smil') self.assertEqual(mms.data_parts[0].data, smil_data) self.assertEqual(mms.data_parts[1].content_type, 'text/plain') self.assertEqual(mms.data_parts[1].data, text_data) self.assertEqual(mms.data_parts[2].content_type, 'image/jpeg') # XXX: Shouldn't it be 'Name' instead ? self.assertEqual(mms.data_parts[2].content_type_parameters, {'name': 'gnu-head.jpg'}) def test_decoding_projekt_exempel_mms(self): path = os.path.join(DATA_DIR, 'projekt_exempel.mms') mms = MMSMessage.from_file(path) self.assertTrue(isinstance(mms, MMSMessage)) headers = { 'Sender-Visibility': 'Show', 'From': '', 'Read-Reply': False, 'Message-Class': 'Personal', 'Transaction-Id': '4-fc60', 'MMS-Version': '1.0', 'Priority': 'Normal', 'To': '12345/TYPE=PLMN', 'Delivery-Report': False, 'Message-Type': 'm-send-req', 'Date': datetime.datetime(2004, 5, 23, 15, 13, 40), 'Content-Type': ('application/vnd.wap.multipart.related', {'Start': '', 'Type': 'application/smil'}), 'Subject': 'Hej', } smil_data = '\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n' text_data = 'Jonatan \xc3\xa4r en GNU' self.assertEqual(mms.headers, headers) self.assertEqual(len(mms.data_parts), 3) self.assertEqual(mms.content_type, 'application/vnd.wap.multipart.related') self.assertEqual(mms.data_parts[0].content_type, 'text/plain') self.assertEqual(mms.data_parts[0].data, text_data) self.assertEqual(mms.data_parts[1].content_type, 'image/gif') self.assertEqual(mms.data_parts[2].content_type, 'application/smil') self.assertEqual(mms.data_parts[2].data, smil_data) self.assertEqual(mms.data_parts[2].content_type_parameters, {'Charset': 'utf-8', 'Name': 'mms.smil'}) def test_decoding_m_mms(self): path = os.path.join(DATA_DIR, 'm.mms') mms = MMSMessage.from_file(path) self.assertTrue(isinstance(mms, MMSMessage)) headers = { 'From': '676f6c64706f737440686f746d61696c2e636f6d'.decode('hex'), 'Transaction-Id': '0000000001', 'MMS-Version': '1.0', 'Message-Type': 'm-retrieve-conf', 'Date': datetime.datetime(2002, 8, 9, 13, 8, 2), 'Content-Type': ('application/vnd.wap.multipart.related', {'Start': '', 'Type': 'application/smil'}), 'Subject': 'GOLD', } text_data1 = 'Audio' text_data2 = 'Text +' text_data3 = 'tagtag.com/gold\r\n' text_data4 = 'globalisierunglobalisierunglobalisierunglobalisierunglobalisierunglobalisierunglobalisierungnureisilabolg' text_data5 = 'KLONE\r\nKLONE\r\n' text_data6 = 'pr\xe4sentiert..' text_data7 = 'GOLD' smil_data = '\r\n\r\n\r\n\r\n\r\n\r\n\r\n \r\n \r\n\r\n\r\n\r\n\r\n \r\n \r\n\r\n\r\n\r\n\r\n \r\n \r\n\r\n\r\n\r\n\r\n \r\n \r\n\r\n\r\n\r\n\r\n\r\n \r\n \r\n\r\n\r\n\r\n\r\n \r\n \r\n\r\n\r\n\r\n\r\n\r\n \r\n \r\n\r\n\r\n' self.assertEqual(mms.headers, headers) self.assertEqual(len(mms.data_parts), 9) self.assertEqual(mms.content_type, 'application/vnd.wap.multipart.related') self.assertEqual(mms.data_parts[0].content_type, 'text/plain') self.assertEqual(mms.data_parts[0].data, text_data1) self.assertEqual(mms.data_parts[0].content_type_parameters, {'Charset': 'us-ascii'}) self.assertEqual(mms.data_parts[1].content_type, 'application/smil') self.assertEqual(mms.data_parts[1].data, smil_data) self.assertEqual(mms.data_parts[1].content_type_parameters, {'Charset': 'us-ascii'}) self.assertEqual(mms.data_parts[2].content_type, 'text/plain') self.assertEqual(mms.data_parts[2].data, text_data2) self.assertEqual(mms.data_parts[2].content_type_parameters, {'Charset': 'us-ascii'}) self.assertEqual(mms.data_parts[3].content_type, 'text/plain') self.assertEqual(mms.data_parts[3].data, text_data3) self.assertEqual(mms.data_parts[3].content_type_parameters, {'Charset': 'us-ascii'}) self.assertEqual(mms.data_parts[4].content_type, 'audio/amr') self.assertEqual(mms.data_parts[5].content_type, 'text/plain') self.assertEqual(mms.data_parts[5].data, text_data4) self.assertEqual(mms.data_parts[5].content_type_parameters, {'Charset': 'us-ascii'}) self.assertEqual(mms.data_parts[6].content_type, 'text/plain') self.assertEqual(mms.data_parts[6].data, text_data5) self.assertEqual(mms.data_parts[6].content_type_parameters, {'Charset': 'us-ascii'}) self.assertEqual(mms.data_parts[7].content_type, 'text/plain') self.assertEqual(mms.data_parts[7].data, text_data6) self.assertEqual(mms.data_parts[7].content_type_parameters, {'Charset': 'us-ascii'}) self.assertEqual(mms.data_parts[8].content_type, 'text/plain') self.assertEqual(mms.data_parts[8].data, text_data7) self.assertEqual(mms.data_parts[8].content_type_parameters, {'Charset': 'us-ascii'}) def test_decoding_27d0a048cd79555de05283a22372b0eb_mms(self): path = os.path.join(DATA_DIR, '27d0a048cd79555de05283a22372b0eb.mms') mms = MMSMessage.from_file(path) self.assertTrue(isinstance(mms, MMSMessage)) headers = { 'Sender-Visibility': 'Show', 'From': '', 'Read-Reply': False, 'Message-Class': 'Personal', 'Transaction-Id': '3-31cb', 'MMS-Version': '1.0', 'Priority': 'Normal', 'To': '123/TYPE=PLMN', 'Delivery-Report': False, 'Message-Type': 'm-send-req', 'Date': datetime.datetime(2004, 5, 23, 14, 14, 58), 'Content-Type': ('application/vnd.wap.multipart.related', {'Start': '', 'Type': 'application/smil'}), 'Subject': 'Angående art-tillhörighet', #'Subject': 'Ang\xc3\xa5ende art-tillh\xc3\xb6righet', } smil_data = '\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n' text_data = 'Jonatan \xc3\xa4r en gnu.' self.assertEqual(mms.headers, headers) self.assertEqual(len(mms.data_parts), 3) self.assertEqual(mms.content_type, 'application/vnd.wap.multipart.related') self.assertEqual(mms.data_parts[0].content_type, 'image/vnd.wap.wbmp') self.assertEqual(mms.data_parts[0].content_type_parameters, {'Name': 'Rain.wbmp'}) self.assertEqual(mms.data_parts[1].content_type, 'text/plain') self.assertEqual(mms.data_parts[1].data, text_data) self.assertEqual(mms.data_parts[1].content_type_parameters, {'Charset': 'utf-8', 'Name': 'mms.txt'}) self.assertEqual(mms.data_parts[2].content_type, 'application/smil') self.assertEqual(mms.data_parts[2].data, smil_data) self.assertEqual(mms.data_parts[2].content_type_parameters, {'Charset': 'utf-8', 'Name': 'mms.smil'}) def test_decoding_SEC_SGHS300M(self): path = os.path.join(DATA_DIR, 'SEC-SGHS300M.mms') mms = MMSMessage.from_file(path) self.assertTrue(isinstance(mms, MMSMessage)) headers = { 'Sender-Visibility': 'Show', 'From': '', 'Read-Reply': False, 'Message-Class': 'Personal', 'Transaction-Id': '31887', 'MMS-Version': '1.0', 'To': '303733383334353636342f545950453d504c4d4e'.decode('hex'), 'Delivery-Report': False, 'Message-Type': 'm-send-req', 'Subject': 'IL', 'Content-Type': ('application/vnd.wap.multipart.mixed', {}), } text_data = 'HV' self.assertEqual(mms.headers, headers) self.assertEqual(len(mms.data_parts), 1) self.assertEqual(mms.content_type, 'application/vnd.wap.multipart.mixed') self.assertEqual(mms.data_parts[0].content_type, 'text/plain') self.assertEqual(mms.data_parts[0].data, text_data) self.assertEqual(mms.data_parts[0].content_type_parameters, {'Charset': 'utf-8'}) def test_encoding_m_sendnotifyresp_ind(self): message = MMSMessage() message.headers['Transaction-Id'] = 'NOK5AIdhfTMYSG4JeIgAAsHtp72AGAAAAAAAA' message.headers['Message-Type'] = 'm-notifyresp-ind' message.headers['Status'] = 'Retrieved' data = [ 140, 131, 152, 78, 79, 75, 53, 65, 73, 100, 104, 102, 84, 77, 89, 83, 71, 52, 74, 101, 73, 103, 65, 65, 115, 72, 116, 112, 55, 50, 65, 71, 65, 65, 65, 65, 65, 65, 65, 65, 0, 141, 144, 149, 129, 132, 163, 1, 35, 129] self.assertEqual(list(message.encode()[:50]), data) python-messaging-0.5.12/messaging/sms/0000775000175000017500000000000011707534031017423 5ustar achiangachiangpython-messaging-0.5.12/messaging/sms/submit.py0000664000175000017500000002404411707534031021304 0ustar achiangachiang# See LICENSE """Classes for sending SMS""" from datetime import datetime, timedelta import re from messaging.sms import consts from messaging.utils import (debug, encode_str, clean_number, pack_8bits_to_ucs2, pack_8bits_to_7bits, pack_8bits_to_8bit, timedelta_to_relative_validity, datetime_to_absolute_validity) from messaging.sms.base import SmsBase from messaging.sms.gsm0338 import is_gsm_text from messaging.sms.pdu import Pdu VALID_NUMBER = re.compile("^\+?\d{3,20}$") class SmsSubmit(SmsBase): """I am a SMS ready to be sent""" def __init__(self, number, text): super(SmsSubmit, self).__init__() self._number = None self._csca = None self._klass = None self._validity = None self.request_status = False self.ref = None self.rand_id = None self.id_list = range(0, 255) self.msgvp = 0xaa self.pid = 0x00 self.number = number self.text = text self.text_gsm = None def _set_number(self, number): if number and not VALID_NUMBER.match(number): raise ValueError("Invalid number format: %s" % number) self._number = number number = property(lambda self: self._number, _set_number) def _set_csca(self, csca): if csca and not VALID_NUMBER.match(csca): raise ValueError("Invalid csca format: %s" % csca) self._csca = csca csca = property(lambda self: self._csca, _set_csca) def _set_validity(self, validity): if validity is None or isinstance(validity, (timedelta, datetime)): # valid values are None, timedelta and datetime self._validity = validity else: raise TypeError("Don't know what to do with %s" % validity) validity = property(lambda self: self._validity, _set_validity) def _set_klass(self, klass): if not isinstance(klass, int): raise TypeError("_set_klass only accepts int objects") if klass not in [0, 1, 2, 3]: raise ValueError("class must be between 0 and 3") self._klass = klass klass = property(lambda self: self._klass, _set_klass) def to_pdu(self): """Returns a list of :class:`~messaging.pdu.Pdu` objects""" smsc_pdu = self._get_smsc_pdu() sms_submit_pdu = self._get_sms_submit_pdu() tpmessref_pdu = self._get_tpmessref_pdu() sms_phone_pdu = self._get_phone_pdu() tppid_pdu = self._get_tppid_pdu() sms_msg_pdu = self._get_msg_pdu() if len(sms_msg_pdu) == 1: pdu = smsc_pdu len_smsc = len(smsc_pdu) / 2 pdu += sms_submit_pdu pdu += tpmessref_pdu pdu += sms_phone_pdu pdu += tppid_pdu pdu += sms_msg_pdu[0] debug("smsc_pdu: %s" % smsc_pdu) debug("sms_submit_pdu: %s" % sms_submit_pdu) debug("tpmessref_pdu: %s" % tpmessref_pdu) debug("sms_phone_pdu: %s" % sms_phone_pdu) debug("tppid_pdu: %s" % tppid_pdu) debug("sms_msg_pdu: %s" % sms_msg_pdu) debug("-" * 20) debug("full_pdu: %s" % pdu) debug("full_text: %s" % self.text) debug("-" * 20) return [Pdu(pdu, len_smsc)] # multipart SMS sms_submit_pdu = self._get_sms_submit_pdu(udh=True) pdu_list = [] cnt = len(sms_msg_pdu) for i, sms_msg_pdu_item in enumerate(sms_msg_pdu): pdu = smsc_pdu len_smsc = len(smsc_pdu) / 2 pdu += sms_submit_pdu pdu += tpmessref_pdu pdu += sms_phone_pdu pdu += tppid_pdu pdu += sms_msg_pdu_item debug("smsc_pdu: %s" % smsc_pdu) debug("sms_submit_pdu: %s" % sms_submit_pdu) debug("tpmessref_pdu: %s" % tpmessref_pdu) debug("sms_phone_pdu: %s" % sms_phone_pdu) debug("tppid_pdu: %s" % tppid_pdu) debug("sms_msg_pdu: %s" % sms_msg_pdu_item) debug("-" * 20) debug("full_pdu: %s" % pdu) debug("full_text: %s" % self.text) debug("-" * 20) pdu_list.append(Pdu(pdu, len_smsc, cnt=cnt, seq=i + 1)) return pdu_list def _get_smsc_pdu(self): if not self.csca or not self.csca.strip(): return "00" number = clean_number(self.csca) ptype = 0x81 # set to unknown number by default if number[0] == '+': number = number[1:] ptype = 0x91 if len(number) % 2: number += 'F' ps = chr(ptype) for n in range(0, len(number), 2): num = number[n + 1] + number[n] ps += chr(int(num, 16)) pl = len(ps) ps = chr(pl) + ps return encode_str(ps) def _get_tpmessref_pdu(self): if self.ref is None: self.ref = self._get_rand_id() self.ref &= 0xFF return encode_str(chr(self.ref)) def _get_phone_pdu(self): number = clean_number(self.number) ptype = 0x81 if number[0] == '+': number = number[1:] ptype = 0x91 pl = len(number) if len(number) % 2: number += 'F' ps = chr(ptype) for n in range(0, len(number), 2): num = number[n + 1] + number[n] ps += chr(int(num, 16)) ps = chr(pl) + ps return encode_str(ps) def _get_tppid_pdu(self): return encode_str(chr(self.pid)) def _get_sms_submit_pdu(self, udh=False): sms_submit = 0x1 if self.validity is None: # handle no validity pass elif isinstance(self.validity, datetime): # handle absolute validity sms_submit |= 0x18 elif isinstance(self.validity, timedelta): # handle relative validity sms_submit |= 0x10 if self.request_status: sms_submit |= 0x20 if udh: sms_submit |= 0x40 return encode_str(chr(sms_submit)) def _get_msg_pdu(self): # Data coding scheme if self.fmt is None: if is_gsm_text(self.text): self.fmt = 0x00 else: self.fmt = 0x08 self.dcs = self.fmt if self.klass is not None: if self.klass == 0: self.dcs |= 0x10 elif self.klass == 1: self.dcs |= 0x11 elif self.klass == 2: self.dcs |= 0x12 elif self.klass == 3: self.dcs |= 0x13 dcs_pdu = encode_str(chr(self.dcs)) # Validity period msgvp_pdu = "" if self.validity is None: # handle no validity pass elif isinstance(self.validity, timedelta): # handle relative msgvp = timedelta_to_relative_validity(self.validity) msgvp_pdu = encode_str(chr(msgvp)) elif isinstance(self.validity, datetime): # handle absolute msgvp = datetime_to_absolute_validity(self.validity) msgvp_pdu = ''.join(map(encode_str, map(chr, msgvp))) # UDL + UD message_pdu = "" if self.fmt == 0x00: self.text_gsm = self.text.encode("gsm0338") if len(self.text_gsm) <= consts.SEVENBIT_SIZE: message_pdu = [pack_8bits_to_7bits(self.text_gsm)] else: message_pdu = self._split_sms_message(self.text_gsm) elif self.fmt == 0x04: if len(self.text) <= consts.EIGHTBIT_SIZE: message_pdu = [pack_8bits_to_8bit(self.text)] else: message_pdu = self._split_sms_message(self.text) elif self.fmt == 0x08: if len(self.text) <= consts.UCS2_SIZE: message_pdu = [pack_8bits_to_ucs2(self.text)] else: message_pdu = self._split_sms_message(self.text) else: raise ValueError("Unknown data coding scheme: %d" % self.fmt) ret = [] for msg in message_pdu: ret.append(dcs_pdu + msgvp_pdu + msg) return ret def _split_sms_message(self, text): if self.fmt == 0x00: len_without_udh = consts.SEVENBIT_MP_SIZE limit = consts.SEVENBIT_SIZE packing_func = pack_8bits_to_7bits total_len = len(self.text_gsm) elif self.fmt == 0x04: len_without_udh = consts.EIGHTBIT_MP_SIZE limit = consts.EIGHTBIT_SIZE packing_func = pack_8bits_to_8bit total_len = len(self.text) elif self.fmt == 0x08: len_without_udh = consts.UCS2_MP_SIZE limit = consts.UCS2_SIZE packing_func = pack_8bits_to_ucs2 total_len = len(self.text) msgs = [] pi, pe = 0, len_without_udh while pi < total_len: if text[pi:pe][-1] == '\x1b': pe -= 1 msgs.append(text[pi:pe]) pi = pe pe += len_without_udh pdu_msgs = [] udh_len = 0x05 mid = 0x00 data_len = 0x03 sms_ref = self._get_rand_id() if self.rand_id is None else self.rand_id sms_ref &= 0xFF for i, msg in enumerate(msgs): i += 1 total_parts = len(msgs) if limit == consts.SEVENBIT_SIZE: udh = (chr(udh_len) + chr(mid) + chr(data_len) + chr(sms_ref) + chr(total_parts) + chr(i)) padding = " " else: udh = (unichr(int("%04x" % ((udh_len << 8) | mid), 16)) + unichr(int("%04x" % ((data_len << 8) | sms_ref), 16)) + unichr(int("%04x" % ((total_parts << 8) | i), 16))) padding = "" pdu_msgs.append(packing_func(padding + msg, udh)) return pdu_msgs def _get_rand_id(self): if not self.id_list: self.id_list = range(0, 255) return self.id_list.pop(0) python-messaging-0.5.12/messaging/sms/pdu.py0000664000175000017500000000032411707534031020564 0ustar achiangachiang# see LICENSE class Pdu(object): def __init__(self, pdu, len_smsc, cnt=1, seq=1): self.pdu = pdu.upper() self.length = len(pdu) / 2 - len_smsc self.cnt = cnt self.seq = seq python-messaging-0.5.12/messaging/sms/__init__.py0000664000175000017500000000031311707534031021531 0ustar achiangachiang# See LICENSE from messaging.sms.submit import SmsSubmit from messaging.sms.deliver import SmsDeliver from messaging.sms.gsm0338 import is_gsm_text __all__ = ["SmsSubmit", "SmsDeliver", "is_gsm_text"] python-messaging-0.5.12/messaging/sms/udh.py0000664000175000017500000000442011707534031020555 0ustar achiangachiang# See LICENSE class PortAddress(object): def __init__(self, dest_port, orig_port, eight_bits): self.dest_port = dest_port self.orig_port = orig_port self.eight_bits = eight_bits def __repr__(self): args = (self.dest_port, self.orig_port) return "" % args class ConcatReference(object): def __init__(self, ref, cnt, seq, eight_bits): self.ref = ref self.cnt = cnt self.seq = seq self.eight_bits = eight_bits def __repr__(self): args = (self.ref, self.cnt, self.seq) return "" % args class UserDataHeader(object): def __init__(self): self.concat = None self.ports = None self.headers = {} def __repr__(self): args = (self.headers, self.concat, self.ports) return "" % args @classmethod def from_status_report_ref(cls, ref): udh = cls() udh.concat = ConcatReference(ref, 0, 0, True) return udh @classmethod def from_bytes(cls, data): udh = cls() while len(data): iei = data.pop(0) ie_len = data.pop(0) ie_data = data[:ie_len] data = data[ie_len:] udh.headers[iei] = ie_data if iei == 0x00: # process SM concatenation 8bit ref. ref, cnt, seq = ie_data udh.concat = ConcatReference(ref, cnt, seq, True) if iei == 0x08: # process SM concatenation 16bit ref. ref = ie_data[0] << 8 | ie_data[1] cnt = ie_data[2] seq = ie_data[3] udh.concat = ConcatReference(ref, cnt, seq, False) elif iei == 0x04: # process App port addressing 8bit dest_port, orig_port = ie_data udh.ports = PortAddress(dest_port, orig_port, False) elif iei == 0x05: # process App port addressing 16bit dest_port = ie_data[0] << 8 | ie_data[1] orig_port = ie_data[2] << 8 | ie_data[3] udh.ports = PortAddress(dest_port, orig_port, False) return udh python-messaging-0.5.12/messaging/sms/wap.py0000664000175000017500000000162511707534031020570 0ustar achiangachiang# See LICENSE from array import array from messaging.mms.mms_pdu import MMSDecoder def is_a_wap_push_notification(s): if not isinstance(s, str): raise TypeError("data must be an array.array serialised to string") data = array("B", s) try: return data[1] == 0x06 except IndexError: return False def extract_push_notification(s): data = array("B", s) wap_push, offset = data[1:3] assert wap_push == 0x06 offset += 3 data = data[offset:] # XXX: Not all WAP pushes are MMS return MMSDecoder().decode_data(data) def is_mms_notification(push): # XXX: Pretty poor, but until we decode generic WAP pushes # it will have to suffice. Ideally we would read the # content-type from the WAP push header and test return (push.headers.get('From') is not None and push.headers.get('Content-Location') is not None) python-messaging-0.5.12/messaging/sms/gsm0338.py0000664000175000017500000002633511707534031021112 0ustar achiangachiang# This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import codecs import sys import traceback # data from # http://snoops.roy202.org/testerman/browser/trunk/plugins/codecs/gsm0338.py # default GSM 03.38 -> unicode def_regular_decode_dict = { '\x00': u'\u0040', # COMMERCIAL AT '\x01': u'\u00A3', # POUND SIGN '\x02': u'\u0024', # DOLLAR SIGN '\x03': u'\u00A5', # YEN SIGN '\x04': u'\u00E8', # LATIN SMALL LETTER E WITH GRAVE '\x05': u'\u00E9', # LATIN SMALL LETTER E WITH ACUTE '\x06': u'\u00F9', # LATIN SMALL LETTER U WITH GRAVE '\x07': u'\u00EC', # LATIN SMALL LETTER I WITH GRAVE '\x08': u'\u00F2', # LATIN SMALL LETTER O WITH GRAVE '\x09': u'\u00C7', # LATIN CAPITAL LETTER C WITH CEDILLA # The Unicode page suggests this is a mistake: but # it's still in the latest version of the spec and # our implementation has to be exact. '\x0A': u'\u000A', # LINE FEED '\x0B': u'\u00D8', # LATIN CAPITAL LETTER O WITH STROKE '\x0C': u'\u00F8', # LATIN SMALL LETTER O WITH STROKE '\x0D': u'\u000D', # CARRIAGE RETURN '\x0E': u'\u00C5', # LATIN CAPITAL LETTER A WITH RING ABOVE '\x0F': u'\u00E5', # LATIN SMALL LETTER A WITH RING ABOVE '\x10': u'\u0394', # GREEK CAPITAL LETTER DELTA '\x11': u'\u005F', # LOW LINE '\x12': u'\u03A6', # GREEK CAPITAL LETTER PHI '\x13': u'\u0393', # GREEK CAPITAL LETTER GAMMA '\x14': u'\u039B', # GREEK CAPITAL LETTER LAMDA '\x15': u'\u03A9', # GREEK CAPITAL LETTER OMEGA '\x16': u'\u03A0', # GREEK CAPITAL LETTER PI '\x17': u'\u03A8', # GREEK CAPITAL LETTER PSI '\x18': u'\u03A3', # GREEK CAPITAL LETTER SIGMA '\x19': u'\u0398', # GREEK CAPITAL LETTER THETA '\x1A': u'\u039E', # GREEK CAPITAL LETTER XI '\x1C': u'\u00C6', # LATIN CAPITAL LETTER AE '\x1D': u'\u00E6', # LATIN SMALL LETTER AE '\x1E': u'\u00DF', # LATIN SMALL LETTER SHARP S (German) '\x1F': u'\u00C9', # LATIN CAPITAL LETTER E WITH ACUTE '\x20': u'\u0020', # SPACE '\x21': u'\u0021', # EXCLAMATION MARK '\x22': u'\u0022', # QUOTATION MARK '\x23': u'\u0023', # NUMBER SIGN '\x24': u'\u00A4', # CURRENCY SIGN '\x25': u'\u0025', # PERCENT SIGN '\x26': u'\u0026', # AMPERSAND '\x27': u'\u0027', # APOSTROPHE '\x28': u'\u0028', # LEFT PARENTHESIS '\x29': u'\u0029', # RIGHT PARENTHESIS '\x2A': u'\u002A', # ASTERISK '\x2B': u'\u002B', # PLUS SIGN '\x2C': u'\u002C', # COMMA '\x2D': u'\u002D', # HYPHEN-MINUS '\x2E': u'\u002E', # FULL STOP '\x2F': u'\u002F', # SOLIDUS '\x30': u'\u0030', # DIGIT ZERO '\x31': u'\u0031', # DIGIT ONE '\x32': u'\u0032', # DIGIT TWO '\x33': u'\u0033', # DIGIT THREE '\x34': u'\u0034', # DIGIT FOUR '\x35': u'\u0035', # DIGIT FIVE '\x36': u'\u0036', # DIGIT SIX '\x37': u'\u0037', # DIGIT SEVEN '\x38': u'\u0038', # DIGIT EIGHT '\x39': u'\u0039', # DIGIT NINE '\x3A': u'\u003A', # COLON '\x3B': u'\u003B', # SEMICOLON '\x3C': u'\u003C', # LESS-THAN SIGN '\x3D': u'\u003D', # EQUALS SIGN '\x3E': u'\u003E', # GREATER-THAN SIGN '\x3F': u'\u003F', # QUESTION MARK '\x40': u'\u00A1', # INVERTED EXCLAMATION MARK '\x41': u'\u0041', # LATIN CAPITAL LETTER A '\x42': u'\u0042', # LATIN CAPITAL LETTER B '\x43': u'\u0043', # LATIN CAPITAL LETTER C '\x44': u'\u0044', # LATIN CAPITAL LETTER D '\x45': u'\u0045', # LATIN CAPITAL LETTER E '\x46': u'\u0046', # LATIN CAPITAL LETTER F '\x47': u'\u0047', # LATIN CAPITAL LETTER G '\x48': u'\u0048', # LATIN CAPITAL LETTER H '\x49': u'\u0049', # LATIN CAPITAL LETTER I '\x4A': u'\u004A', # LATIN CAPITAL LETTER J '\x4B': u'\u004B', # LATIN CAPITAL LETTER K '\x4C': u'\u004C', # LATIN CAPITAL LETTER L '\x4D': u'\u004D', # LATIN CAPITAL LETTER M '\x4E': u'\u004E', # LATIN CAPITAL LETTER N '\x4F': u'\u004F', # LATIN CAPITAL LETTER O '\x50': u'\u0050', # LATIN CAPITAL LETTER P '\x51': u'\u0051', # LATIN CAPITAL LETTER Q '\x52': u'\u0052', # LATIN CAPITAL LETTER R '\x53': u'\u0053', # LATIN CAPITAL LETTER S '\x54': u'\u0054', # LATIN CAPITAL LETTER T '\x55': u'\u0055', # LATIN CAPITAL LETTER U '\x56': u'\u0056', # LATIN CAPITAL LETTER V '\x57': u'\u0057', # LATIN CAPITAL LETTER W '\x58': u'\u0058', # LATIN CAPITAL LETTER X '\x59': u'\u0059', # LATIN CAPITAL LETTER Y '\x5A': u'\u005A', # LATIN CAPITAL LETTER Z '\x5B': u'\u00C4', # LATIN CAPITAL LETTER A WITH DIAERESIS '\x5C': u'\u00D6', # LATIN CAPITAL LETTER O WITH DIAERESIS '\x5D': u'\u00D1', # LATIN CAPITAL LETTER N WITH TILDE '\x5E': u'\u00DC', # LATIN CAPITAL LETTER U WITH DIAERESIS '\x5F': u'\u00A7', # SECTION SIGN '\x60': u'\u00BF', # INVERTED QUESTION MARK '\x61': u'\u0061', # LATIN SMALL LETTER A '\x62': u'\u0062', # LATIN SMALL LETTER B '\x63': u'\u0063', # LATIN SMALL LETTER C '\x64': u'\u0064', # LATIN SMALL LETTER D '\x65': u'\u0065', # LATIN SMALL LETTER E '\x66': u'\u0066', # LATIN SMALL LETTER F '\x67': u'\u0067', # LATIN SMALL LETTER G '\x68': u'\u0068', # LATIN SMALL LETTER H '\x69': u'\u0069', # LATIN SMALL LETTER I '\x6A': u'\u006A', # LATIN SMALL LETTER J '\x6B': u'\u006B', # LATIN SMALL LETTER K '\x6C': u'\u006C', # LATIN SMALL LETTER L '\x6D': u'\u006D', # LATIN SMALL LETTER M '\x6E': u'\u006E', # LATIN SMALL LETTER N '\x6F': u'\u006F', # LATIN SMALL LETTER O '\x70': u'\u0070', # LATIN SMALL LETTER P '\x71': u'\u0071', # LATIN SMALL LETTER Q '\x72': u'\u0072', # LATIN SMALL LETTER R '\x73': u'\u0073', # LATIN SMALL LETTER S '\x74': u'\u0074', # LATIN SMALL LETTER T '\x75': u'\u0075', # LATIN SMALL LETTER U '\x76': u'\u0076', # LATIN SMALL LETTER V '\x77': u'\u0077', # LATIN SMALL LETTER W '\x78': u'\u0078', # LATIN SMALL LETTER X '\x79': u'\u0079', # LATIN SMALL LETTER Y '\x7A': u'\u007A', # LATIN SMALL LETTER Z '\x7B': u'\u00E4', # LATIN SMALL LETTER A WITH DIAERESIS '\x7C': u'\u00F6', # LATIN SMALL LETTER O WITH DIAERESIS '\x7D': u'\u00F1', # LATIN SMALL LETTER N WITH TILDE '\x7E': u'\u00FC', # LATIN SMALL LETTER U WITH DIAERESIS '\x7F': u'\u00E0', # LATIN SMALL LETTER A WITH GRAVE } # default GSM 03.38 escaped characters -> unicode def_escape_decode_dict = { '\x0A': u'\u000C', # FORM FEED '\x14': u'\u005E', # CIRCUMFLEX ACCENT '\x28': u'\u007B', # LEFT CURLY BRACKET '\x29': u'\u007D', # RIGHT CURLY BRACKET '\x2F': u'\u005C', # REVERSE SOLIDUS '\x3C': u'\u005B', # LEFT SQUARE BRACKET '\x3D': u'\u007E', # TILDE '\x3E': u'\u005D', # RIGHT SQUARE BRACKET '\x40': u'\u007C', # VERTICAL LINE '\x65': u'\u20AC', # EURO SIGN } # Replacement characters, default is question mark. Used when it is not too # important to ensure exact UTF-8 -> GSM -> UTF-8 equivilence, such as when # humans read and write SMS. But for USSD and other M2M applications it's # important to ensure the conversion is exact. def_replace_encode_dict = { u'\u00E7': '\x09', # LATIN SMALL LETTER C WITH CEDILLA u'\u0391': '\x41', # GREEK CAPITAL LETTER ALPHA u'\u0392': '\x42', # GREEK CAPITAL LETTER BETA u'\u0395': '\x45', # GREEK CAPITAL LETTER EPSILON u'\u0397': '\x48', # GREEK CAPITAL LETTER ETA u'\u0399': '\x49', # GREEK CAPITAL LETTER IOTA u'\u039A': '\x4B', # GREEK CAPITAL LETTER KAPPA u'\u039C': '\x4D', # GREEK CAPITAL LETTER MU u'\u039D': '\x4E', # GREEK CAPITAL LETTER NU u'\u039F': '\x4F', # GREEK CAPITAL LETTER OMICRON u'\u03A1': '\x50', # GREEK CAPITAL LETTER RHO u'\u03A4': '\x54', # GREEK CAPITAL LETTER TAU u'\u03A7': '\x58', # GREEK CAPITAL LETTER CHI u'\u03A5': '\x59', # GREEK CAPITAL LETTER UPSILON u'\u0396': '\x5A', # GREEK CAPITAL LETTER ZETA } QUESTION_MARK = chr(0x3f) # unicode -> default GSM 03.38 def_regular_encode_dict = \ dict((u, g) for g, u in def_regular_decode_dict.iteritems()) # unicode -> default escaped GSM 03.38 characters def_escape_encode_dict = \ dict((u, g) for g, u in def_escape_decode_dict.iteritems()) def encode(input_, errors='strict'): """ :type input_: unicode :return: string """ result = [] for c in input_: try: result.append(def_regular_encode_dict[c]) except KeyError: if c in def_escape_encode_dict: # OK, let's encode it as an escaped characters result.append('\x1b') result.append(def_escape_encode_dict[c]) else: if errors == 'strict': raise UnicodeError("Invalid GSM character") elif errors == 'replace': result.append( def_replace_encode_dict.get(c, QUESTION_MARK)) elif errors == 'ignore': pass else: raise UnicodeError("Unknown error handling") ret = ''.join(result) return ret, len(ret) def decode(input_, errors='strict'): """ :type input_: str :return: unicode """ result = [] index = 0 while index < len(input_): c = input_[index] index += 1 if c == '\x1b': if index < len(input_): c = input_[index] index += 1 result.append(def_escape_decode_dict.get(c, u'\xa0')) else: result.append(u'\xa0') else: try: result.append(def_regular_decode_dict[c]) except KeyError: # error handling: unassigned byte, must be > 0x7f if errors == 'strict': raise UnicodeError("Unrecognized GSM character") elif errors == 'replace': result.append('?') elif errors == 'ignore': pass else: raise UnicodeError("Unknown error handling") ret = u''.join(result) return ret, len(ret) # encodings module API def getregentry(encoding): if encoding == 'gsm0338': return codecs.CodecInfo(name='gsm0338', encode=encode, decode=decode) # Codec registration codecs.register(getregentry) def is_gsm_text(text): """Returns True if ``text`` can be encoded as gsm text""" try: text.encode("gsm0338") except UnicodeError: return False except: traceback.print_exc(file=sys.stdout) return False return True python-messaging-0.5.12/messaging/sms/deliver.py0000664000175000017500000002003011707534031021422 0ustar achiangachiang# see LICENSE """Classes for processing received SMS""" from datetime import datetime, timedelta from messaging.utils import (swap, swap_number, encode_bytes, debug, unpack_msg, unpack_msg2, to_array) from messaging.sms import consts from messaging.sms.base import SmsBase from messaging.sms.udh import UserDataHeader class SmsDeliver(SmsBase): """I am a delivered SMS in your Inbox""" def __init__(self, pdu, strict=True): super(SmsDeliver, self).__init__() self._pdu = None self._strict = strict self.date = None self.mtype = None self.sr = None self.pdu = pdu @property def data(self): """ Returns a dict populated with the SMS attributes It mimics the old API to ease the port to the new API """ ret = { 'text': self.text, 'pid': self.pid, 'dcs': self.dcs, 'csca': self.csca, 'number': self.number, 'type': self.type, 'date': self.date, 'fmt': self.fmt, 'sr': self.sr, } if self.udh is not None: if self.udh.concat is not None: ret.update({ 'ref': self.udh.concat.ref, 'cnt': self.udh.concat.cnt, 'seq': self.udh.concat.seq, }) return ret def _set_pdu(self, pdu): if not self._strict and len(pdu) % 2: # if not strict and PDU-length is odd, remove the last character # and make it even. See the discussion of this bug at # http://github.com/pmarti/python-messaging/issues#issue/7 pdu = pdu[:-1] if len(pdu) % 2: raise ValueError("Can not decode an odd-length pdu") # XXX: Should we keep the original PDU or the modified one? self._pdu = pdu data = to_array(self._pdu) # Service centre address smscl = data.pop(0) if smscl > 0: smscertype = data.pop(0) smscl -= 1 self.csca = swap_number(encode_bytes(data[:smscl])) if (smscertype >> 4) & 0x07 == consts.INTERNATIONAL: self.csca = '+%s' % self.csca data = data[smscl:] else: self.csca = None # 1 byte(octet) == 2 char # Message type TP-MTI bits 0,1 # More messages to send/deliver bit 2 # Status report request indicated bit 5 # User Data Header Indicator bit 6 # Reply path set bit 7 try: self.mtype = data.pop(0) except TypeError: raise ValueError("Decoding this type of SMS is not supported yet") mtype = self.mtype & 0x03 if mtype == 0x02: return self._decode_status_report_pdu(data) if mtype == 0x01: raise ValueError("Cannot decode a SmsSubmitReport message yet") sndlen = data.pop(0) if sndlen % 2: sndlen += 1 sndlen = int(sndlen / 2.0) sndtype = (data.pop(0) >> 4) & 0x07 if sndtype == consts.ALPHANUMERIC: # coded according to 3GPP TS 23.038 [9] GSM 7-bit default alphabet sender = unpack_msg2(data[:sndlen]).decode("gsm0338") else: # Extract phone number of sender sender = swap_number(encode_bytes(data[:sndlen])) if sndtype == consts.INTERNATIONAL: sender = '+%s' % sender self.number = sender data = data[sndlen:] # 1 byte TP-PID (Protocol IDentifier) self.pid = data.pop(0) # 1 byte TP-DCS (Data Coding Scheme) self.dcs = data.pop(0) if self.dcs & (0x04 | 0x08) == 0: self.fmt = 0x00 elif self.dcs & 0x04: self.fmt = 0x04 elif self.dcs & 0x08: self.fmt = 0x08 datestr = '' # Get date stamp (sender's local time) date = list(encode_bytes(data[:6])) for n in range(1, len(date), 2): date[n - 1], date[n] = date[n], date[n - 1] data = data[6:] # Get sender's offset from GMT (TS 23.040 TP-SCTS) tz = data.pop(0) offset = ((tz & 0x07) * 10 + ((tz & 0xf0) >> 4)) * 15 if (tz & 0x08): offset = offset * -1 # 02/08/26 19:37:41 datestr = "%s%s/%s%s/%s%s %s%s:%s%s:%s%s" % tuple(date) outputfmt = '%y/%m/%d %H:%M:%S' sndlocaltime = datetime.strptime(datestr, outputfmt) sndoffset = timedelta(minutes=offset) # date as UTC self.date = sndlocaltime - sndoffset self._process_message(data) def _process_message(self, data): # Now get message body msgl = data.pop(0) msg = encode_bytes(data[:msgl]) # check for header headlen = ud_len = 0 if self.mtype & 0x40: # UDHI present ud_len = data.pop(0) self.udh = UserDataHeader.from_bytes(data[:ud_len]) headlen = (ud_len + 1) * 8 if self.fmt == 0x00: while headlen % 7: headlen += 1 headlen /= 7 headlen = int(headlen) if self.fmt == 0x00: # XXX: Use unpack_msg2 data = data[ud_len:].tolist() #self.text = unpack_msg2(data).decode("gsm0338") self.text = unpack_msg(msg)[headlen:msgl].decode("gsm0338") elif self.fmt == 0x04: self.text = data[ud_len:].tostring() elif self.fmt == 0x08: data = data[ud_len:].tolist() _bytes = [int("%02X%02X" % (data[i], data[i + 1]), 16) for i in range(0, len(data), 2)] self.text = u''.join(list(map(unichr, _bytes))) pdu = property(lambda self: self._pdu, _set_pdu) def _decode_status_report_pdu(self, data): self.udh = UserDataHeader.from_status_report_ref(data.pop(0)) sndlen = data.pop(0) if sndlen % 2: sndlen += 1 sndlen = int(sndlen / 2.0) sndtype = data.pop(0) recipient = swap_number(encode_bytes(data[:sndlen])) if (sndtype >> 4) & 0x07 == consts.INTERNATIONAL: recipient = '+%s' % recipient data = data[sndlen:] date = swap(list(encode_bytes(data[:7]))) try: scts_str = "%s%s/%s%s/%s%s %s%s:%s%s:%s%s" % tuple(date[0:12]) self.date = datetime.strptime(scts_str, "%y/%m/%d %H:%M:%S") except (ValueError, TypeError): scts_str = '' debug('Could not decode scts: %s' % date) data = data[7:] date = swap(list(encode_bytes(data[:7]))) try: dt_str = "%s%s/%s%s/%s%s %s%s:%s%s:%s%s" % tuple(date[0:12]) dt = datetime.strptime(dt_str, "%y/%m/%d %H:%M:%S") except (ValueError, TypeError): dt_str = '' dt = None debug('Could not decode date: %s' % date) data = data[7:] msg_l = [recipient, scts_str] try: status = data.pop(0) except IndexError: # Yes it is entirely possible that a status report comes # with no status at all! I'm faking for now the values and # set it to SR-UNKNOWN as that's all we can do _status = None status = 0x1 sender = 'SR-UNKNOWN' msg_l.append(dt_str) else: _status = status if status == 0x00: msg_l.append(dt_str) else: msg_l.append('') if status == 0x00: sender = "SR-OK" elif status == 0x1: sender = "SR-UNKNOWN" elif status == 0x30: sender = "SR-STORED" else: sender = "SR-UNKNOWN" self.number = sender self.text = "|".join(msg_l) self.fmt = 0x08 # UCS2 self.type = 0x03 # status report self.sr = { 'recipient': recipient, 'scts': self.date, 'dt': dt, 'status': _status } python-messaging-0.5.12/messaging/sms/base.py0000664000175000017500000000040611707534031020707 0ustar achiangachiang# see LICENSE class SmsBase(object): def __init__(self): self.udh = None self.number = None self.text = None self.fmt = None self.dcs = None self.pid = None self.csca = None self.type = None python-messaging-0.5.12/messaging/sms/consts.py0000664000175000017500000000047111707534031021310 0ustar achiangachiang# see LICENSE SEVENBIT_SIZE = 160 EIGHTBIT_SIZE = 140 UCS2_SIZE = 70 SEVENBIT_MP_SIZE = SEVENBIT_SIZE - 7 EIGHTBIT_MP_SIZE = EIGHTBIT_SIZE - 6 UCS2_MP_SIZE = UCS2_SIZE - 3 # address type UNKNOWN = 0 INTERNATIONAL = 1 NATIONAL = 2 NETWORK_SPECIFIC = 3 SUBSCRIBER = 4 ALPHANUMERIC = 5 ABBREVIATED = 6 RESERVED = 7 python-messaging-0.5.12/messaging/mms/0000775000175000017500000000000011707534031017415 5ustar achiangachiangpython-messaging-0.5.12/messaging/mms/mms_pdu.py0000664000175000017500000010646411707534031021446 0ustar achiangachiang# This library is free software. # # It was originally distributed under the terms of the GNU Lesser # General Public License Version 2. # # python-messaging opts to apply the terms of the ordinary GNU # General Public License v2, as permitted by section 3 of the LGPL # v2.1. This re-licensing allows the entirety of python-messaging to # be distributed according to the terms of GPL-2. # # See the COPYING file included in this archive """MMS Data Unit structure encoding and decoding classes""" from __future__ import with_statement import array import os import random from messaging.utils import debug from messaging.mms import message, wsp_pdu from messaging.mms.iterator import PreviewIterator def flatten_list(x): """Flattens ``x`` into a single list""" result = [] for el in x: if hasattr(el, "__iter__") and not isinstance(el, basestring): result.extend(flatten_list(el)) else: result.append(el) return result mms_field_names = { 0x01: ('Bcc', 'encoded_string_value'), 0x02: ('Cc', 'encoded_string_value'), 0x03: ('Content-Location', 'uri_value'), 0x04: ('Content-Type', 'content_type_value'), 0x05: ('Date', 'date_value'), 0x06: ('Delivery-Report', 'boolean_value'), 0x07: ('Delivery-Time', 'delivery_time_value'), 0x08: ('Expiry', 'expiry_value'), 0x09: ('From', 'from_value'), 0x0a: ('Message-Class', 'message_class_value'), 0x0b: ('Message-ID', 'text_string'), 0x0c: ('Message-Type', 'message_type_value'), 0x0d: ('MMS-Version', 'version_value'), 0x0e: ('Message-Size', 'long_integer'), 0x0f: ('Priority', 'priority_value'), 0x10: ('Read-Reply', 'boolean_value'), 0x11: ('Report-Allowed', 'boolean_value'), 0x12: ('Response-Status', 'response_status_value'), 0x13: ('Response-Text', 'encoded_string_value'), 0x14: ('Sender-Visibility', 'sender_visibility_value'), 0x15: ('Status', 'status_value'), 0x16: ('Subject', 'encoded_string_value'), 0x17: ('To', 'encoded_string_value'), 0x18: ('Transaction-Id', 'text_string'), } class MMSDecoder(wsp_pdu.Decoder): """A decoder for MMS messages""" def __init__(self, filename=None): """ :param filename: If specified, decode the content of the MMS message file with this name :type filename: str """ self._mms_data = array.array('B') self._mms_message = message.MMSMessage() self._parts = [] def decode_file(self, filename): """ Load the data contained in the specified ``filename``, and decode it. :param filename: The name of the MMS message file to open :type filename: str :raise OSError: The filename is invalid :return: The decoded MMS data :rtype: MMSMessage """ num_bytes = os.stat(filename)[6] data = array.array('B') with open(filename, 'rb') as f: data.fromfile(f, num_bytes) return self.decode_data(data) def decode_data(self, data): """ Decode the specified MMS message data :param data: The MMS message data to decode :type data: array.array('B') :return: The decoded MMS data :rtype: MMSMessage """ self._mms_message = message.MMSMessage() self._mms_data = data body_iter = self.decode_message_header() self.decode_message_body(body_iter) return self._mms_message def decode_message_header(self): """ Decodes the (full) MMS header data This must be called before :func:`_decodeBody`, as it sets certain internal variables relating to data lengths, etc. """ data_iter = PreviewIterator(self._mms_data) # First 3 headers (in order ############################ # - X-Mms-Message-Type # - X-Mms-Transaction-ID # - X-Mms-Version # TODO: reimplement strictness - currently we allow these 3 headers # to be mixed with any of the other headers (this allows the # decoding of "broken" MMSs, but is technically incorrect) # Misc headers ############## # The next few headers will not be in a specific order, except for # "Content-Type", which should be the last header # According to [4], MMS header field names will be short integers content_type_found = False header = '' while content_type_found == False: try: header, value = self.decode_header(data_iter) except StopIteration: break if header == mms_field_names[0x04][0]: content_type_found = True else: self._mms_message.headers[header] = value if header == 'Content-Type': # Otherwise it might break Content-Location # content_type, params = value self._mms_message.headers[header] = value return data_iter def decode_message_body(self, data_iter): """ Decodes the MMS message body :param data_iter: an iterator over the sequence of bytes of the MMS body :type data_iter: iter """ ######### MMS body: headers ########### # Get the number of data parts in the MMS body try: num_entries = self.decode_uint_var(data_iter) except StopIteration: return #print 'Number of data entries (parts) in MMS body:', num_entries ########## MMS body: entries ########## # For every data "part", we have to read the following sequence: # , # , # , # for part_num in xrange(num_entries): #print '\nPart %d:\n------' % part_num headers_len = self.decode_uint_var(data_iter) data_len = self.decode_uint_var(data_iter) # Prepare to read content-type + other possible headers ct_field_bytes = [] for i in xrange(headers_len): ct_field_bytes.append(data_iter.next()) ct_iter = PreviewIterator(ct_field_bytes) # Get content type ctype, ct_parameters = self.decode_content_type_value(ct_iter) headers = {'Content-Type': (ctype, ct_parameters)} # Now read other possible headers until bytes # have been read while True: try: hdr, value = self.decode_header(ct_iter) headers[hdr] = value except StopIteration: break # Data (note: this is not null-terminated) data = array.array('B') for i in xrange(data_len): data.append(data_iter.next()) part = message.DataPart() part.set_data(data, ctype) part.content_type_parameters = ct_parameters part.headers = headers self._mms_message.add_data_part(part) @staticmethod def decode_header(byte_iter): """ Decodes a header entry from an MMS message starting at the byte pointed to by :func:`byte_iter.next` From [4], section 7.1:: Header = MMS-header | Application-header The return type of the "header value" depends on the header itself; it is thus up to the function calling this to determine what that type is (or at least compensate for possibly different return value types). :raise DecodeError: This uses :func:`decode_mms_header` and :func:`decode_application_header`, and will raise this exception under the same circumstances as :func:`decode_application_header`. ``byte_iter`` will not be modified in this case. :return: The decoded header entry from the MMS, in the format: (, ) :rtype: tuple """ try: return MMSDecoder.decode_mms_header(byte_iter) except wsp_pdu.DecodeError: return wsp_pdu.Decoder.decode_header(byte_iter) @staticmethod def decode_mms_header(byte_iter): """ Decodes the MMS header pointed by ``byte_iter`` This method takes into account the assigned number values for MMS field names, as specified in [4], section 7.3, table 8. From [4], section 7.1:: MMS-header = MMS-field-name MMS-value MMS-field-name = Short-integer MMS-value = Bcc-value | Cc-value | Content-location-value | Content-type-value | etc :raise wsp_pdu.DecodeError: The MMS field name could not be parsed. ``byte_iter`` will not be modified. :return: The decoded MMS header, in the format: (, ) :rtype: tuple """ # Get the MMS-field-name mms_field_name = '' preview = byte_iter.preview() byte = wsp_pdu.Decoder.decode_short_integer_from_byte(preview) if byte in mms_field_names: byte_iter.next() mms_field_name = mms_field_names[byte][0] else: byte_iter.reset_preview() raise wsp_pdu.DecodeError('Invalid MMS Header: could ' 'not decode MMS field name') # Now get the MMS-value mms_value = '' try: name = mms_field_names[byte][1] mms_value = getattr(MMSDecoder, 'decode_%s' % name)(byte_iter) except wsp_pdu.DecodeError, msg: raise wsp_pdu.DecodeError('Invalid MMS Header: Could ' 'not decode MMS-value: %s' % msg) except: raise RuntimeError('A fatal error occurred, probably due to an ' 'unimplemented decoding operation. Tried to ' 'decode header: %s' % mms_field_name) return mms_field_name, mms_value @staticmethod def decode_encoded_string_value(byte_iter): """ Decodes the encoded string value pointed by ``byte_iter`` From [4], section 7.2.9:: Encoded-string-value = Text-string | Value-length Char-set Text-string The Char-set values are registered by IANA as MIBEnum value. This function is not fully implemented, in that it does not have proper support for the Char-set values; it basically just reads over that sequence of bytes, and ignores it (see code for details) - any help with this will be greatly appreciated. :return: The decoded text string :rtype: str """ try: # First try "Value-length Char-set Text-string" value_length = wsp_pdu.Decoder.decode_value_length(byte_iter) # TODO: add proper support for charsets... try: charset = wsp_pdu.Decoder.decode_well_known_charset(byte_iter) except wsp_pdu.DecodeError, msg: raise Exception('encoded_string_value decoding error - ' 'Could not decode Charset value: %s' % msg) return wsp_pdu.Decoder.decode_text_string(byte_iter) except wsp_pdu.DecodeError: # Fall back on just "Text-string" return wsp_pdu.Decoder.decode_text_string(byte_iter) @staticmethod def decode_boolean_value(byte_iter): """ Decodes the boolean value pointed by ``byte_iter`` From [4], section 7.2.6:: Delivery-report-value = Yes | No Yes = No = A lot of other yes/no fields use this encoding (read-reply, report-allowed, etc) :raise wsp_pdu.DecodeError: The boolean value could not be parsed. ``byte_iter`` will not be modified. :return: The value for the field :rtype: bool """ byte = byte_iter.preview() if byte not in (128, 129): byte_iter.reset_preview() raise wsp_pdu.DecodeError('Error parsing boolean value ' 'for byte: %s' % hex(byte)) byte = byte_iter.next() return byte == 128 @staticmethod def decode_delivery_time_value(byte_iter): value_length = wsp_pdu.Decoder.decode_value_length(byte_iter) token = byte_iter.next() value = wsp_pdu.Decoder.decode_long_integer(byte_iter) if token == 128: token_type = 'absolute' elif token == 129: token_type = 'relative' else: raise wsp_pdu.DecodeError('Delivery-Time type token value is undefined' ' (%s), should be either 128 or 129' % token) return (token_type, value) @staticmethod def decode_from_value(byte_iter): """ Decodes the "From" value pointed by ``byte_iter`` From [4], section 7.2.11:: From-value = Value-length (Address-present-token Encoded-string-value | Insert-address-token ) Address-present-token = Insert-address-token = :return: The "From" address value :rtype: str """ value_length = wsp_pdu.Decoder.decode_value_length(byte_iter) # See what token we have byte = byte_iter.next() if byte == 129: # Insert-address-token return '' return MMSDecoder.decode_encoded_string_value(byte_iter) @staticmethod def decode_message_class_value(byte_iter): """ Decodes the "Message-Class" value pointed by ``byte_iter`` From [4], section 7.2.12:: Message-class-value = Class-identifier | Token-text Class-identifier = Personal | Advertisement | Informational | Auto Personal = Advertisement = Informational = Auto = The token-text is an extension method to the message class. :return: The decoded message class :rtype: str """ class_identifiers = { 128: 'Personal', 129: 'Advertisement', 130: 'Informational', 131: 'Auto', } byte = byte_iter.preview() if byte in class_identifiers: byte_iter.next() return class_identifiers[byte] byte_iter.reset_preview() return wsp_pdu.Decoder.decode_token_text(byte_iter) @staticmethod def decode_message_type_value(byte_iter): """ Decodes the "Message-Type" value pointed by ``byte_iter`` Defined in [4], section 7.2.14. :return: The decoded message type, or '' :rtype: str """ message_types = { 0x80: 'm-send-req', 0x81: 'm-send-conf', 0x82: 'm-notification-ind', 0x83: 'm-notifyresp-ind', 0x84: 'm-retrieve-conf', 0x85: 'm-acknowledge-ind', 0x86: 'm-delivery-ind', } byte = byte_iter.preview() if byte in message_types: byte_iter.next() return message_types[byte] byte_iter.reset_preview() return '' @staticmethod def decode_priority_value(byte_iter): """ Decode the "Priority" value pointed by ``byte_iter`` Defined in [4], section 7.2.17 :raise wsp_pdu.DecodeError: The priority value could not be decoded; ``byte_iter`` is not modified in this case. :return: The decoded priority value :rtype: str """ priorities = {128: 'Low', 129: 'Normal', 130: 'High'} byte = byte_iter.preview() if byte in priorities: byte = byte_iter.next() return priorities[byte] byte_iter.reset_preview() raise wsp_pdu.DecodeError('Error parsing Priority value ' 'for byte: %s' % byte) @staticmethod def decode_sender_visibility_value(byte_iter): """ Decodes the sender visibility value pointed by ``byte_iter`` Defined in [4], section 7.2.22:: Sender-visibility-value = Hide | Show Hide = Show = :raise wsp_pdu.DecodeError: The sender visibility value could not be parsed. ``byte_iter`` will not be modified in this case. :return: The sender visibility: 'Hide' or 'Show' :rtype: str """ byte = byte_iter.preview() if byte not in (128, 129): byte_iter.reset_preview() raise wsp_pdu.DecodeError('Error parsing sender visibility ' 'value for byte: %s' % hex(byte)) byte = byte_iter.next() value = 'Hide' if byte == 128 else 'Show' return value @staticmethod def decode_response_status_value(byte_iter): """ Decodes the "Response Status" value pointed by ``byte_iter`` Defined in [4], section 7.2.20 :raise wsp_pdu.DecodeError: The sender visibility value could not be parsed. ``byte_iter`` will not be modified in this case. :return: The decoded Response-status-value :rtype: str """ response_status_values = { 0x80: 'Ok', 0x81: 'Error-unspecified', 0x82: 'Error-service-denied', 0x83: 'Error-message-format-corrupt', 0x84: 'Error-sending-address-unresolved', 0x85: 'Error-message-not-found', 0x86: 'Error-network-problem', 0x87: 'Error-content-not-accepted', 0x88: 'Error-unsupported-message', } byte = byte_iter.preview() byte_iter.next() # Return error unspecified if it couldn't be decoded return response_status_values.get(byte, 0x81) @staticmethod def decode_status_value(byte_iter): """ Used to decode the "Status" MMS header. Defined in [4], section 7.2.23 :raise wsp_pdu.DecodeError: The sender visibility value could not be parsed. ``byte_iter`` will not be modified in this case. :return: The decoded Status-value :rtype: str """ status_values = { 0x80: 'Expired', 0x81: 'Retrieved', 0x82: 'Rejected', 0x83: 'Deferred', 0x84: 'Unrecognised', } byte = byte_iter.next() # Return an unrecognised state if it couldn't be decoded return status_values.get(byte, 0x84) @staticmethod def decode_expiry_value(byte_iter): """ Used to decode the "Expiry" MMS header. From [4], section 7.2.10:: Expiry-value = Value-length (Absolute-token Date-value | Relative-token Delta-seconds-value) Absolute-token = Relative-token = :raise wsp_pdu.DecodeError: The Expiry-value could not be decoded :return: The decoded Expiry-value, either as a date, or as a delta-seconds value :rtype: str or int """ value_length = MMSDecoder.decode_value_length(byte_iter) token = byte_iter.next() if token == 0x80: # Absolute-token return MMSDecoder.decode_date_value(byte_iter) elif token == 0x81: # Relative-token return MMSDecoder.decode_delta_seconds_value(byte_iter) raise wsp_pdu.DecodeError('Unrecognized token value: %s' % hex(token)) class MMSEncoder(wsp_pdu.Encoder): """MMS Encoder""" def __init__(self): self._mms_message = message.MMSMessage() def encode(self, mms_message): """ Encodes the specified MMS message ``mms_message`` :param mms_message: The MMS message to encode :type mms_message: MMSMessage :return: The binary-encoded MMS data, as a sequence of bytes :rtype: array.array('B') """ self._mms_message = mms_message msg_data = self.encode_message_header() msg_data.extend(self.encode_message_body()) return msg_data def encode_message_header(self): """ Binary-encodes the MMS header data. The encoding used for the MMS header is specified in [4]. All "constant" encoded values found/used in this method are also defined in [4]. For a good example, see [2]. :return: the MMS PDU header, as an array of bytes :rtype: array.array('B') """ # See [4], chapter 8 for info on how to use these # from_types = {'Address-present-token': 0x80, # 'Insert-address-token': 0x81} # content_types = {'application/vnd.wap.multipart.related': 0xb3} # Create an array of 8-bit values message_header = array.array('B') headers_to_encode = self._mms_message.headers # If the user added any of these to the message manually # (X- prefix) use those instead for hdr in ('X-Mms-Message-Type', 'X-Mms-Transaction-Id', 'X-Mms-Version'): if hdr in headers_to_encode: if hdr == 'X-Mms-Version': clean_header = 'MMS-Version' else: clean_header = hdr.replace('X-Mms-', '', 1) headers_to_encode[clean_header] = headers_to_encode[hdr] del headers_to_encode[hdr] # First 3 headers (in order), according to [4]: ################################################ # - X-Mms-Message-Type # - X-Mms-Transaction-ID # - X-Mms-Version ### Start of Message-Type verification if 'Message-Type' not in headers_to_encode: # Default to 'm-retrieve-conf'; we don't need a To/CC field for # this (see WAP-209, section 6.3, table 5) headers_to_encode['Message-Type'] = 'm-retrieve-conf' # See if the chosen message type is valid, given the message's # other headers. NOTE: we only distinguish between 'm-send-req' # (requires a destination number) and 'm-retrieve-conf' # (requires no destination number) - if "Message-Type" is # something else, we assume the message creator knows # what she is doing if headers_to_encode['Message-Type'] == 'm-send-req': found_dest_address = False for address_type in ('To', 'Cc', 'Bc'): if address_type in headers_to_encode: found_dest_address = True break if not found_dest_address: headers_to_encode['Message-Type'] = 'm-retrieve-conf' ### End of Message-Type verification ### Start of Transaction-Id verification if 'Transaction-Id' not in headers_to_encode: trans_id = str(random.randint(1000, 9999)) headers_to_encode['Transaction-Id'] = trans_id ### End of Transaction-Id verification ### Start of MMS-Version verification if 'MMS-Version' not in headers_to_encode: headers_to_encode['MMS-Version'] = '1.0' # Encode the first three headers, in correct order for hdr in ('Message-Type', 'Transaction-Id', 'MMS-Version'): message_header.extend( MMSEncoder.encode_header(hdr, headers_to_encode[hdr])) del headers_to_encode[hdr] # Encode all remaining MMS message headers, except "Content-Type" # -- this needs to be added last, according [2] and [4] for hdr in headers_to_encode: if hdr != 'Content-Type': message_header.extend( MMSEncoder.encode_header(hdr, headers_to_encode[hdr])) # Ok, now only "Content-type" should be left content_type, ct_parameters = headers_to_encode['Content-Type'] message_header.extend(MMSEncoder.encode_mms_field_name('Content-Type')) ret = MMSEncoder.encode_content_type_value(content_type, ct_parameters) message_header.extend(flatten_list(ret)) return message_header def encode_message_body(self): """ Binary-encodes the MMS body data The MMS body's header should not be confused with the actual MMS header, as returned by :func:`encode_header`. The encoding used for the MMS body is specified in [5], section 8.5. It is only referenced in [4], however [2] provides a good example of how this ties in with the MMS header encoding. The MMS body is of type `application/vnd.wap.multipart` ``mixed`` or ``related``. As such, its structure is divided into a header, and the data entries/parts:: [ header ][ entries ] ^^^^^^^^^^^^^^^^^^^^^ MMS Body The MMS Body header consists of one entry[5]:: name type purpose ------- ------- ----------- num_entries uint_var num of entries in the multipart entity The MMS body's multipart entries structure:: name type purpose ------- ----- ----------- HeadersLen uint_var length of the ContentType and Headers fields combined DataLen uint_var length of the Data field ContentType Multiple octets the content type of the data Headers ( - length of ) octets the part's headers Data octets the part's data :return: The binary-encoded MMS PDU body, as an array of bytes :rtype: array.array('B') """ message_body = array.array('B') #TODO: enable encoding of MMSs without SMIL file ########## MMS body: header ########## # Parts: SMIL file + num_entries = 1 for page in self._mms_message._pages: num_entries += page.number_of_parts() for data_part in self._mms_message._data_parts: num_entries += 1 message_body.extend(self.encode_uint_var(num_entries)) ########## MMS body: entries ########## # For every data "part", we have to add the following sequence: # , # , # , # . # Gather the data parts, adding the MMS message's SMIL file smil_part = message.DataPart() smil = self._mms_message.smil() smil_part.set_data(smil, 'application/smil') #TODO: make this dynamic.... smil_part.headers['Content-ID'] = '<0000>' parts = [smil_part] for slide in self._mms_message._pages: for part_tuple in (slide.image, slide.audio, slide.text): if part_tuple is not None: parts.append(part_tuple[0]) for part in parts: name, val_type = part.headers['Content-Type'] part_content_type = self.encode_content_type_value(name, val_type) encoded_part_headers = [] for hdr in part.headers: if hdr == 'Content-Type': continue encoded_part_headers.extend( wsp_pdu.Encoder.encode_header(hdr, part.headers[hdr])) # HeadersLen entry (length of the ContentType and # Headers fields combined) headers_len = len(part_content_type) + len(encoded_part_headers) message_body.extend(self.encode_uint_var(headers_len)) # DataLen entry (length of the Data field) message_body.extend(self.encode_uint_var(len(part))) # ContentType entry message_body.extend(part_content_type) # Headers message_body.extend(encoded_part_headers) # Data (note: we do not null-terminate this) for char in part.data: message_body.append(ord(char)) return message_body @staticmethod def encode_header(header_field_name, header_value): """ Encodes a header entry for an MMS message The return type of the "header value" depends on the header itself; it is thus up to the function calling this to determine what that type is (or at least compensate for possibly different return value types). From [4], section 7.1:: Header = MMS-header | Application-header MMS-header = MMS-field-name MMS-value MMS-field-name = Short-integer MMS-value = Bcc-value | Cc-value | Content-location-value | Content-type-value | etc :raise DecodeError: This uses :func:`decode_mms_header` and :func:`decode_application_header`, and will raise this exception under the same circumstances as :func:`decode_application_header`. ``byte_iter`` will not be modified in this case. :return: The decoded header entry from the MMS, in the format: (, ) :rtype: tuple """ encoded_header = [] # First try encoding the header as a "MMS-header"... for assigned_number in mms_field_names: header = mms_field_names[assigned_number][0] if header == header_field_name: encoded_header.extend( wsp_pdu.Encoder.encode_short_integer(assigned_number)) # Now encode the value expected_type = mms_field_names[assigned_number][1] try: ret = getattr(MMSEncoder, 'encode_%s' % expected_type)(header_value) encoded_header.extend(ret) except wsp_pdu.EncodeError, msg: raise wsp_pdu.EncodeError('Error encoding parameter ' 'value: %s' % msg) except: debug('A fatal error occurred, probably due to an ' 'unimplemented encoding operation') raise break # See if the "MMS-header" encoding worked if not len(encoded_header): # ...it didn't. Use "Application-header" encoding header_name = wsp_pdu.Encoder.encode_token_text(header_field_name) encoded_header.extend(header_name) # Now add the value encoded_header.extend( wsp_pdu.Encoder.encode_text_string(header_value)) return encoded_header @staticmethod def encode_mms_field_name(field_name): """ Encodes an MMS header field name From [4], section 7.1:: MMS-field-name = Short-integer :raise EncodeError: The specified header field name is not a well-known MMS header. :param field_name: The header field name to encode :type field_name: str :return: The encoded header field name, as a sequence of bytes :rtype: list """ encoded_mms_field_name = [] for assigned_number in mms_field_names: if mms_field_names[assigned_number][0] == field_name: encoded_mms_field_name.extend( wsp_pdu.Encoder.encode_short_integer(assigned_number)) break if not len(encoded_mms_field_name): raise wsp_pdu.EncodeError('The specified header field name is not ' 'a well-known MMS header field name') return encoded_mms_field_name @staticmethod def encode_from_value(from_value=''): """ Encodes the "From" address value From [4], section 7.2.11:: From-value = Value-length (Address-present-token Encoded-string-value | Insert-address-token ) Address-present-token = Insert-address-token = :param from_value: The "originator" of the MMS message. This may be an empty string, in which case a token will be encoded informing the MMSC to insert the address of the device that sent this message (default). :type from_value: str :return: The encoded "From" address value, as a sequence of bytes :rtype: list """ encoded_from_value = [] if len(from_value) == 0: value_length = wsp_pdu.Encoder.encode_value_length(1) encoded_from_value.extend(value_length) encoded_from_value.append(129) # Insert-address-token else: encoded_address = MMSEncoder.encode_encoded_string_value(from_value) # the "+1" is for the Address-present-token length = len(encoded_address) + 1 value_length = wsp_pdu.Encoder.encode_value_length(length) encoded_from_value.extend(value_length) encoded_from_value.append(128) # Address-present-token encoded_from_value.extend(encoded_address) return encoded_from_value @staticmethod def encode_encoded_string_value(string_value): """ Encodes ``string_value`` This is a simple wrapper to :func:`encode_text_string` From [4], section 7.2.9:: Encoded-string-value = Text-string | Value-length Char-set Text-string The Char-set values are registered by IANA as MIBEnum value. :param string_value: The text string to encode :type string_value: str :return: The encoded string value, as a sequence of bytes :rtype: list """ return wsp_pdu.Encoder.encode_text_string(string_value) @staticmethod def encode_message_type_value(message_type): """ Encodes the Message-Type value ``message_type`` Unknown message types are discarded; thus they will be encoded as 0x80 ("m-send-req") by this function Defined in [4], section 7.2.14. :param message_type: The MMS message type to encode :type message_type: str :return: The encoded message type, as a sequence of bytes :rtype: list """ message_types = { 'm-send-req': 0x80, 'm-send-conf': 0x81, 'm-notification-ind': 0x82, 'm-notifyresp-ind': 0x83, 'm-retrieve-conf': 0x84, 'm-acknowledge-ind': 0x85, 'm-delivery-ind': 0x86, } return [message_types.get(message_type, 0x80)] @staticmethod def encode_status_value(status_value): status_values = { 'Expired': 0x80, 'Retrieved': 0x81, 'Rejected': 0x82, 'Deferred': 0x83, 'Unrecognised': 0x84, } # Return an unrecognised state if it couldn't be decoded return [status_values.get(status_value, 'Unrecognised')] python-messaging-0.5.12/messaging/mms/__init__.py0000664000175000017500000000566311707534031021540 0ustar achiangachiang# This library is free software. # # It was originally distributed under the terms of the GNU Lesser # General Public License Version 2. # # python-messaging opts to apply the terms of the ordinary GNU # General Public License v2, as permitted by section 3 of the LGPL # v2.1. This re-licensing allows the entirety of python-messaging to # be distributed according to the terms of GPL-2. # # See the COPYING file included in this archive # # Copyright (C) 2007 Francois Aucamp # """ Multimedia Messaging Service (MMS) library The :mod:`messaging.mms` module provides several classes for the creation and manipulation of MMS messages (multimedia messages) used in mobile devices such as cellular telephones. Multimedia Messaging Service (MMS) is a messaging service for the mobile environment standardized by the WAP Forum and 3GPP. To the end-user MMS is very similar to the text-based Short Message Service (SMS): it provides automatic immediate delivery for user-created content from device to device. In addition to text, however, MMS messages can contain multimedia content such as still images, audio clips and video clips, which are binded together into a "mini presentation" (or slideshow) that controls for example, the order in which images are to appear on the screen, how long they will be displayed, when an audio clip should be played, etc. Furthermore, MMS messages do not have the 160-character limit of SMS messages. An MMS message is a multimedia presentation in one entity; it is not a text file with attachments. This library enables the creation of MMS messages with full support for presentation layout, and multimedia data parts such as JPEG, GIF, AMR, MIDI, 3GP, etc. It also allows the decoding and unpacking of received MMS messages. @version: 0.2 @author: Francois Aucamp C{} @license: GNU General Public License, version 2 @note: References used in the code and this document: .. [1] MMS Conformance Document version 2.0.0, 6 February 2002 U{www.bogor.net/idkf/bio2/mobile-docs/mms_conformance_v2_0_0.pdf} .. [2] Forum Nokia, "How To Create MMS Services, Version 4.0" U{http://forum.nokia.com/info/sw.nokia.com/id/a57a4f20-b7f2-475b-b426-19eff18a5afb/How_To_Create_MMS_Services_v4_0_en2.pdf.html} .. [3] Wap Forum/Open Mobile ALliance, "WAP-206 MMS Client Transactions" U{http://www.openmobilealliance.org/tech/affiliates/LicenseAgreement.asp?DocName=/wap/wap-206-mmsctr-20020115-a.pdf} .. [4] Wap Forum/Open Mobile Alliance, "WAP-209 MMS Encapsulation Protocol" U{http://www.openmobilealliance.org/tech/affiliates/LicenseAgreement.asp?DocName=/wap/wap-209-mmsencapsulation-20020105-a.pdf} .. [5] Wap Forum/Open Mobile Alliance, "WAP-230 Wireless Session Protocol Specification" U{http://www.openmobilealliance.org/tech/affiliates/LicenseAgreement.asp?DocName=/wap/wap-230-wsp-20010705-a.pdf} .. [6] IANA: "Character Sets" U{http://www.iana.org/assignments/character-sets} """ python-messaging-0.5.12/messaging/mms/iterator.py0000664000175000017500000000515311707534031021624 0ustar achiangachiang# This library is free software. # # It was originally distributed under the terms of the GNU Lesser # General Public License Version 2. # # python-messaging opts to apply the terms of the ordinary GNU # General Public License v2, as permitted by section 3 of the LGPL # v2.1. This re-licensing allows the entirety of python-messaging to # be distributed according to the terms of GPL-2. # # See the COPYING file included in this archive # # The docstrings in this module contain epytext markup; API documentation # may be created by processing this file with epydoc: http://epydoc.sf.net """Iterator with "value preview" capability.""" class PreviewIterator(object): """An ``iter`` wrapper class providing a "previewable" iterator. This "preview" functionality allows the iterator to return successive values from its ``iterable`` object, without actually mvoving forward itself. This is very usefuly if the next item(s) in an iterator must be used for something, after which the iterator should "undo" those read operations, so that they can be read again by another function. From the user point of view, this class supersedes the builtin iter() function: like iter(), it is called as PreviewIter(iterable). """ def __init__(self, data): self._it = iter(data) self._cached_values = [] self._preview_pos = 0 def __iter__(self): return self def next(self): self.reset_preview() if len(self._cached_values) > 0: return self._cached_values.pop(0) else: return self._it.next() def preview(self): """ Return the next item in the ``iteratable`` object But it does not modify the actual iterator (i.e. do not intefere with :func:`next`. Successive calls to :func:`preview` will return successive values from the ``iterable`` object, exactly in the same way :func:`next` does. However, :func:`preview` will always return the next item from ``iterable`` after the item returned by the previous :func:`preview` or :func:`next` call, whichever was called the most recently. To force the "preview() iterator" to synchronize with the "next() iterator" (without calling :func:`next`), use :func:`reset_preview`. """ if self._preview_pos < len(self._cached_values): value = self._cached_values[self._preview_pos] else: value = self._it.next() self._cached_values.append(value) self._preview_pos += 1 return value def reset_preview(self): self._preview_pos = 0 python-messaging-0.5.12/messaging/mms/wsp_pdu.py0000664000175000017500000021761711707534031021466 0ustar achiangachiang# This library is free software. # # It was originally distributed under the terms of the GNU Lesser # General Public License Version 2. # # python-messaging opts to apply the terms of the ordinary GNU # General Public License v2, as permitted by section 3 of the LGPL # v2.1. This re-licensing allows the entirety of python-messaging to # be distributed according to the terms of GPL-2. # # See the COPYING file included in this archive # # The docstrings in this module contain epytext markup; API documentation # may be created by processing this file with epydoc: http://epydoc.sf.net """ WSP Data Unit structure encoding and decoding classes Throughout the classes defined in this module, the following "primitive data type" terminology applies, as specified in [5], section 8.1.1:: Data Type Definition bit 1 bit of data octet 8 bits of opaque data uint8 8-bit unsigned integer uint16 16-bit unsigned integer uint32 32-bit unsigned integer uintvar variable length unsigned integer This Encoder and Decoder classes provided in this module firstly provides public methods for decoding and encoding each of these data primitives (where needed). Next, they provide methods encapsulating the basic WSP Header encoding rules as defined in section 8.4.2.1 of [5]. Finally, the classes defined here provide methods for decoding/parsing specific WSP header fields. References used in the code and this document: [5] Wap Forum/Open Mobile Alliance, "WAP-230 Wireless Session Protocol Specification" U{http://www.openmobilealliance.org/tech/affiliates/LicenseAgreement.asp?DocName=/wap/wap-230-wsp-20010705-a.pdf} """ import array from datetime import datetime from messaging.utils import debug from messaging.mms.iterator import PreviewIterator wsp_pdu_types = { 0x01: 'Connect', 0x02: 'ConnectReply', 0x03: 'Redirect', 0x04: 'Reply', 0x05: 'Disconnect', 0x06: 'Push', 0x07: 'ConfirmedPush', 0x08: 'Suspend', 0x09: 'Resume', 0x40: 'Get', 0x60: 'Post', } # Well-known parameter assignments ([5], table 38) well_known_parameters = { 0x00: ('Q', 'q_value'), 0x01: ('Charset', 'well_known_charset'), 0x02: ('Level', 'version_value'), 0x03: ('Type', 'integer_value'), 0x05: ('Name', 'text_string'), 0x06: ('Filename', 'text_string'), 0x07: ('Differences', 'Field-name'), 0x08: ('Padding', 'short_integer'), 0x09: ('Type', 'constrained_encoding'), # encoding version 1.2 0x0a: ('Start', 'text_string'), 0x0b: ('Start-info', 'text_string'), 0x0c: ('Comment', 'text_string'), # encoding version 1.3 0x0d: ('Domain', 'text_string'), 0x0e: ('Max-Age', 'delta_seconds_value'), 0x0f: ('Path', 'text_string'), 0x10: ('Secure', 'no_value'), 0x11: ('SEC', 'short_integer'), # encoding version 1.4 0x12: ('MAC', 'text_value'), 0x13: ('Creation-date', 'date_value'), 0x14: ('Modification-date', 'date_value'), 0x15: ('Read-date', 'date_value'), 0x16: ('Size', 'integer_value'), 0x17: ('Name', 'text_value'), 0x18: ('Filename', 'text_value'), 0x19: ('Start', 'text_value'), 0x1a: ('Start-info', 'text_value'), 0x1b: ('Comment', 'text_value'), 0x1c: ('Domain', 'text_value'), 0x1d: ('Path', 'text_value'), } # Content type assignments ([5], table 40) well_known_content_types = [ '*/*', 'text/*', 'text/html', 'text/plain', 'text/x-hdml', 'text/x-ttml', 'text/x-vCalendar', 'text/x-vCard', 'text/vnd.wap.wml', 'text/vnd.wap.wmlscript', 'text/vnd.wap.wta-event', 'multipart/*', 'multipart/mixed', 'multipart/form-data', 'multipart/byterantes', 'multipart/alternative', 'application/*', 'application/java-vm', 'application/x-www-form-urlencoded', 'application/x-hdmlc', 'application/vnd.wap.wmlc', 'application/vnd.wap.wmlscriptc', 'application/vnd.wap.wta-eventc', 'application/vnd.wap.uaprof', 'application/vnd.wap.wtls-ca-certificate', 'application/vnd.wap.wtls-user-certificate', 'application/x-x509-ca-cert', 'application/x-x509-user-cert', 'image/*', 'image/gif', 'image/jpeg', 'image/tiff', 'image/png', 'image/vnd.wap.wbmp', 'application/vnd.wap.multipart.*', 'application/vnd.wap.multipart.mixed', 'application/vnd.wap.multipart.form-data', 'application/vnd.wap.multipart.byteranges', 'application/vnd.wap.multipart.alternative', 'application/xml', 'text/xml', 'application/vnd.wap.wbxml', 'application/x-x968-cross-cert', 'application/x-x968-ca-cert', 'application/x-x968-user-cert', 'text/vnd.wap.si', 'application/vnd.wap.sic', 'text/vnd.wap.sl', 'application/vnd.wap.slc', 'text/vnd.wap.co', 'application/vnd.wap.coc', 'application/vnd.wap.multipart.related', 'application/vnd.wap.sia', 'text/vnd.wap.connectivity-xml', 'application/vnd.wap.connectivity-wbxml', 'application/pkcs7-mime', 'application/vnd.wap.hashed-certificate', 'application/vnd.wap.signed-certificate', 'application/vnd.wap.cert-response', 'application/xhtml+xml', 'application/wml+xml', 'text/css', 'application/vnd.wap.mms-message', 'application/vnd.wap.rollover-certificate', 'application/vnd.wap.locc+wbxml', 'application/vnd.wap.loc+xml', 'application/vnd.syncml.dm+wbxml', 'application/vnd.syncml.dm+xml', 'application/vnd.syncml.notification', 'application/vnd.wap.xhtml+xml', 'application/vnd.wv.csp.cir', 'application/vnd.oma.dd+xml', 'application/vnd.oma.drm.message', 'application/vnd.oma.drm.content', 'application/vnd.oma.drm.rights+xml', 'application/vnd.oma.drm.rights+wbxml', ] # Well-known character sets (table 42 of [5]) # Format { : } # Note that the assigned number is the same as the IANA MIBEnum value # "gsm-default-alphabet" is not included, as it is not assigned any # value in [5]. Also note, this is by no means a complete list well_known_charsets = { 0x07EA: 'big5', 0x03E8: 'iso-10646-ucs-2', 0x04: 'iso-8859-1', 0x05: 'iso-8859-2', 0x06: 'iso-8859-3', 0x07: 'iso-8859-4', 0x08: 'iso-8859-5', 0x09: 'iso-8859-6', 0x0A: 'iso-8859-7', 0x0B: 'iso-8859-8', 0x0C: 'iso-8859-9', 0x11: 'shift_JIS', 0x03: 'us-ascii', 0x6A: 'utf-8', } # Header Field Name assignments ([5], table 39) header_field_names = [ 'Accept', 'Accept-Charset', 'Accept-Encoding', 'Accept-Language', 'Accept-Ranges', 'Age', 'Allow', 'Authorization', 'Cache-Control', 'Connection', 'Content-Base', 'Content-Encoding', 'Content-Language', 'Content-Length', 'Content-Location', 'Content-MD5', 'Content-Range', 'Content-Type', 'Date', 'Etag', 'Expires', 'From', 'Host', 'If-Modified-Since', 'If-Match', 'If-None-Match', 'If-Range', 'If-Unmodified-Since', 'Location', 'Last-Modified', 'Max-Forwards', 'Pragma', 'Proxy-Authenticate', 'Proxy-Authorization', 'Public', 'Range', 'Referer', 'Retry-After', 'Server', 'Transfer-Encoding', 'Upgrade', 'User-Agent', 'Vary', 'Via', 'Warning', 'WWW-Authenticate', 'Content-Disposition', # encoding version 1.2 'X-Wap-Application-Id', 'X-Wap-Content-URI', 'X-Wap-Initiator-URI', 'Accept-Application', 'Bearer-Indication', 'Push-Flag', 'Profile', 'Profile-Diff', 'Profile-Warning', # encoding version 1.3 'Expect', 'TE', 'Trailer', 'Accept-Charset', 'Accept-Encoding', 'Cache-Control', 'Content-Range', 'X-Wap-Tod', 'Content-ID', 'Set-Cookie', 'Cookie', 'Encoding-Version', # encoding version 1.4 'Profile-Warning', 'Content-Disposition', 'X-WAP-Security', 'Cache-Control', ] # TODO: combine this dict with the header_field_names table (same as well # known parameter assignments) # Temporary fix to allow different types of header field values to be # dynamically decoded header_field_encodings = {'Accept': 'accept_value', 'Pragma': 'pragma_value'} def get_header_field_names(version='1.2'): """ Formats list of assigned values for header field names, for the specified WSP encoding version. :param version: The WSP encoding version to use. This defaults to "1.2", but may be "1.1", "1.2", "1.3" or "1.4" (see table 39 in [5] for details). :type version: str :raise ValueError: The specified encoding version is invalid. :return: A list containing the WSP header field names with assigned numbers for the specified encoding version (and lower). :rtype: list """ if version not in ('1.1', '1.2', '1.3', '1.4'): raise ValueError('version must be "1.1",' '"1.2", "1.3" or "1.4"') version = int(version.split('.')[1]) versioned_field_names = header_field_names[:] if version == 3: versioned_field_names = versioned_field_names[:0x44] elif version == 2: versioned_field_names = versioned_field_names[:0x38] elif version == 1: versioned_field_names = versioned_field_names[:0x2f] return versioned_field_names def get_well_known_parameters(version='1.2'): """ Return a list of assigned values for parameter names for ``version`` Formats list of assigned values for well-known parameter names, for the specified WSP encoding version. :param version: The WSP encoding version to use. This defaults to "1.2", but may be "1.1", "1.2", "1.3" or "1.4" (see table 38 in [5] for details). :type version: str :raise ValueError: The specified encoding version is invalid. :return: A dictionary containing the well-known parameters with assigned numbers for the specified encoding version (and lower). Entries in this dict follow the format:: : (, ) :rtype: dict """ if version not in ('1.1', '1.2', '1.3', '1.4'): raise ValueError('version must be "1.1",' '"1.2", "1.3" or "1.4"') else: version = int(version.split('.')[1]) versioned_params = well_known_parameters.copy() if version <= 3: for assigned_number in range(0x11, 0x1e): del versioned_params[assigned_number] if version <= 2: for assigned_number in range(0x0c, 0x11): del versioned_params[assigned_number] if version == 1: for assigned_number in range(0x09, 0x0c): del versioned_params[assigned_number] return versioned_params class DecodeError(Exception): """ Raised when a decoding operation failed Most probably due to an invalid byte in the sequence provided for decoding """ class EncodeError(Exception): """ Raised when an encoding operation failed Most probably due to an invalid value provided for encoding """ class Decoder: """A WSP Data unit decoder""" @staticmethod def decode_uint_8(byte_iter): """ Decodes an 8-bit uint from the byte pointed to by ``byte_iter`` This function will move the iterator passed as ``byte_iter`` one byte forward. :param byte_iter: an iterator over a sequence of bytes :type byte_iter: iter :return: the decoded 8-bit unsigned integer :rtype: int """ # Make the byte unsigned return byte_iter.next() & 0xf @staticmethod def decode_uint_var(byte_iter): """ Decodes the uint starting at the byte pointed to by ``byte_iter`` See :func:`wsp.Encoder.encode_uint_var` for a detailed description of the encoding scheme used for ``uint_var`` sequences. This function will move the iterator passed as ``byte_iter`` to the last octet in the uintvar sequence; thus, after calling this, that iterator's `next()` function will return the first byte **after** the uintvar sequence. :param byte_iter: an iterator over a sequence of bytes :type byte_iter: iter :return: the decoded unsigned integer :rtype: int """ uint = 0 byte = byte_iter.next() while (byte >> 7) == 0x01: uint = uint << 7 uint |= byte & 0x7f byte = byte_iter.next() uint = uint << 7 uint |= byte & 0x7f return uint @staticmethod def decode_short_integer(byte_iter): """ Decodes the short-integer value starting at ``byte_iter`` The encoding for a long integer is specified in [5], section 8.4.2.1:: Short-integer = OCTET Integers in range 0-127 shall be encoded as a one octet value with the most significant bit set to one (1xxx xxxx) and with the value in the remaining least significant bits. :raise DecodeError: Not a valid short-integer; the most significant isn't set to 1. ``byte_iter`` will not be modified if this is raised :return: The decoded short integer :rtype: int """ byte = byte_iter.preview() if not byte & 0x80: byte_iter.reset_preview() raise DecodeError('Not a valid short-integer: MSB not set') byte = byte_iter.next() return byte & 0x7f @staticmethod def decode_short_integer_from_byte(byte): """ Decodes the short-integer value contained in the specified byte value :param byte: the byte value to decode :type byte: int :raise DecodeError: Not a valid short-integer; the MSB isn't set to 1. :return: The decoded short integer :rtype: int """ if not byte & 0x80: raise DecodeError('Not a valid short-integer: MSB not set') return byte & 0x7f @staticmethod def decode_long_integer(byte_iter): """ Decodes the long int value pointed to by ``byte_iter`` If this function returns successfully, it will move the iterator passed as ``byte_iter`` to the last octet in the encoded long integer sequence; thus, after calling this, that iterator's `next()` function will return the first byte **after** the encoded long integer sequence. The encoding for a long integer is specified in [5], section 8.4.2.1, and follows the form:: Long-integer = [Short-length] [Multi-octet-integer] ^^^^^^ ^^^^^^^^^^^^^^^^^^^^^ 1 byte bytes The Short-length indicates the length of the Multi-octet-integer. :raise DecodeError: The byte pointed to by ``byte_iter.next`` does not indicate the start of a valid long-integer sequence (short-length is invalid). If this is raised, the iterator passed as ``byte_iter`` will not be modified. :param byte_iter: an iterator over a sequence of bytes :type byte_iter: iter :return: The decoded long integer :rtype: int """ try: shortLength = Decoder.decode_short_length(byte_iter) except DecodeError: raise DecodeError('short-length byte is invalid') longInt = 0 # Decode the Multi-octect-integer for i in xrange(shortLength): longInt = longInt << 8 longInt |= byte_iter.next() return longInt @staticmethod def decode_text_string(byte_iter): """ Decodes the null-terminated, binary-encoded string value starting at the byte pointed to by ``byte_iter``. this function will move the iterator passed as ``byte_iter`` to the last octet in the encoded string sequence; thus, after calling this, that iterator's `next()` function will return the first byte **after** the encoded string sequence. This follows the basic encoding rules specified in [5], section 8.4.2.1 :param byte_iter: an iterator over a sequence of bytes :type byte_iter: iter :return: The decoded text string :rtype: str """ decoded_string = '' byte = byte_iter.next() # Remove Quote character (octet 127), if present if byte == 127: byte = byte_iter.next() while byte != 0x00: decoded_string += chr(byte) byte = byte_iter.next() return decoded_string @staticmethod def decode_quoted_string(byte_iter): """ From [5], section 8.4.2.1:: Quoted-string = *TEXT End-of-string The TEXT encodes an RFC2616 Quoted-string with the enclosing quotation-marks <"> removed :return: The decoded text string :rtype: str """ # look for the quote character byte = byte_iter.preview() if byte != 34: byte_iter.reset_preview() raise DecodeError('Invalid quoted string: must ' 'start with ') byte_iter.next() # CHECK: should the quotation chars be pre- and appended before # returning *technically* we should not check for quote characters. return Decoder.decode_text_string(byte_iter) @staticmethod def decode_token_text(byte_iter): """ From [5], section 8.4.2.1: Token-text = Token End-of-string :raise DecodeError: invalid token; byte_iter is not modified :return: The token string if successful, otherwise the read byte :rtype: str or int """ separators = (11, 32, 40, 41, 44, 47, 58, 59, 60, 61, 62, 63, 64, 91, 92, 93, 123, 125) token = '' byte = byte_iter.preview() if byte <= 31 or byte in separators: byte_iter.reset_preview() raise DecodeError('Invalid token') byte = byte_iter.next() while byte > 31 and byte not in separators: token += chr(byte) byte = byte_iter.next() return token @staticmethod def decode_extension_media(byte_iter): """ Decode the extension media pointed by ``byte_iter`` This encoding is used for media values, which have no well-known binary encoding From [5], section 8.4.2.1:: Extension-media = *TEXT End-of-string :raise DecodeError: The TEXT started with an invalid character. ``byte_iter`` is not modified if this happens. :return: The decoded media type value :rtype: str """ media_value = '' byte = byte_iter.preview() if byte < 32 or byte == 127: byte_iter.reset_preview() raise DecodeError('Invalid Extension-media: TEXT ' 'starts with invalid character: %d' % byte) byte = byte_iter.next() while byte != 0x00: media_value += chr(byte) byte = byte_iter.next() return media_value @staticmethod def decode_constrained_encoding(byte_iter): """Constrained-encoding = Extension-Media --or-- Short-integer This encoding is used for token values, which have no well-known binary encoding, or when the assigned number of the well-known encoding is small enough to fit into Short-integer. :return: The decoding constrained-encoding token value :rtype: str or int """ result = None try: # First try and see if this is just a short-integer result = Decoder.decode_short_integer(byte_iter) except DecodeError: # Ok, it should be Extension-Media then try: result = Decoder.decode_extension_media(byte_iter) except DecodeError: # Give up raise DecodeError('Not a valid Constrained-encoding sequence') return result @staticmethod def decode_short_length(byte_iter): """ From [5], section 8.4.2.2: Short-length = :raise DecodeError: The byte is not a valid short-length value; it is not in octet range 0-30. In this case, the iterator passed as ``byte_iter`` is not modified. :note: If this function returns successfully, the iterator passed as ``byte_iter`` is moved one byte forward. :return: The decoded short-length :rtype: int """ # Make sure it's a valid short-length byte = byte_iter.preview() if byte > 30: byte_iter.reset_preview() raise DecodeError('Not a valid short-length: ' 'should be in octet range 0-30') return byte_iter.next() @staticmethod def decode_value_length(byte_iter): """ Decodes the value length indicator starting at ``byte_iter`` "Value length" is used to indicate the length of a value to follow, as used in the `Content-Type` header in the MMS body, for example. The encoding for a value length indicator is specified in [5], section 8.4.2.2, and follows the form:: Value-length = [Short-length] --or-- [Length-quote] [Length] ^^^^^^ ^^^^^^ ^^^^^^ 1 byte 1 byte x bytes uint_var-integer :raise DecodeError: The value_length could not be decoded. If this happens, ``byte_iter`` is not modified. :return: The decoded value length indicator :rtype: int """ length_value = 0 # Check for short-length try: length_value = Decoder.decode_short_length(byte_iter) except DecodeError: byte = byte_iter.preview() # CHECK: this strictness MAY cause issues, but it is correct if byte == 31: byte_iter.next() # skip past the length-quote length_value = Decoder.decode_uint_var(byte_iter) else: byte_iter.reset_preview() raise DecodeError('Invalid Value-length: not short-length, ' 'and no length-quote present') return length_value @staticmethod def decode_integer_value(byte_iter): """ Decodes the integer value pointed by ``byte_iter`` From [5], section 8.4.2.3:: Integer-Value = Short-integer | Long-integer If successful, this function will move the iterator passed as ``byte_iter`` to the last octet in the integer value sequence; thus, after calling this, that iterator's `next()` function will return the first byte **after** the integer value sequence. :raise DecodeError: The sequence of bytes starting at ``byte_iter`` does not contain a valid integer value. If this is raised, the iterator is not modified. :return: The decoded integer value :rtype: int """ integer = 0 # First try and see if it's a short-integer try: integer = Decoder.decode_short_integer(byte_iter) except DecodeError: try: integer = Decoder.decode_long_integer(byte_iter) except DecodeError: raise DecodeError('Not a valid integer value') return integer @staticmethod def decode_content_type_value(byte_iter): """ Decodes an encoded content type value. From [5], section 8.4.2.24:: Content-type-value = Constrained-media | Content-general-form The short form of the Content-type-value MUST only be used when the well-known media is in the range of 0-127 or a text string. In all other cases the general form MUST be used. :return: The media type (content type), and a dictionary of parameters to this content type (which is empty if there are no parameters). This parameter dictionary is in the format: {: }. The final returned tuple is in the format: (, ) :rtype: tuple """ # First try do decode it as Constrained-media content_type = '' params = {} try: content_type = Decoder.decode_constrained_media(byte_iter) except DecodeError: # Try the general form content_type, params = Decoder.decode_content_general_form(byte_iter) return content_type, params @staticmethod def decode_well_known_media(byte_iter): """ Decodes the well known media pointed by ``byte_iter`` From [5], section 8.4.2.7:: Well-known-media = Integer-value It is encoded using values from the "Content Type Assignments" table (see [5], table 40). :param byte_iter: an iterator over a sequence of bytes :type byte_iter: iter :raise DecodeError: This is raised if the integer value representing the well-known media type cannot be decoded correctly, or the well-known media type value could not be found in the table of assigned content types. If this exception is raised, the iterator passed as ``byte_iter`` is not modified. If successful, this function will move the iterator passed as ``byte_iter`` to the last octet in the content type value sequence; thus, after calling this, that iterator's `next()` function will return the first byte B{after}the content type value sequence. :return: the decoded MIME content type name :rtype: str """ try: value = Decoder.decode_integer_value(byte_iter) except DecodeError: raise DecodeError('Invalid well-known media: could not read ' 'integer value representing it') try: return well_known_content_types[value] except IndexError: raise DecodeError('Invalid well-known media: could not ' 'find content type in table of assigned values') @staticmethod def decode_media_type(byte_iter): """ Decodes the media type pointed by ``byte_iter`` Used by :func:`decode_content_general_form` From [5], section 8.2.4.24:: Media-type = (Well-known-media | Extension-Media) *(Parameter) :param byte_iter: an iterator over a sequence of bytes :type byte_iter: iter :return: The decoded media type :rtype: str """ try: return Decoder.decode_well_known_media(byte_iter) except DecodeError: return Decoder.decode_extension_media(byte_iter) @staticmethod def decode_constrained_media(byte_iter): """ Decodes the constrained media pointed pointed by ``byte_iter`` It is encoded using values from the "Content Type Assignments" table. From [5], section 8.4.2.7:: Constrained-media = Constrained-encoding :raise DecodeError: Invalid constrained media sequence :return: The decoded media type :rtype: str """ try: media_value = Decoder.decode_constrained_encoding(byte_iter) except DecodeError, msg: raise DecodeError('Invalid Constrained-media: %s' % msg) if isinstance(media_value, int): try: return well_known_content_types[media_value] except IndexError: raise DecodeError('Invalid constrained media: could not ' 'find well-known content type') return media_value @staticmethod def decode_content_general_form(byte_iter): """ Decodes the content general form pointed by ``byte_iter`` From [5], section 8.4.2.24:: Content-general-form = Value-length Media-type Used in decoding Content-type fields and their parameters; see :func:`decode_content_type_value`. Used by :func:`decode_content_type_value`. :return: The media type (content type), and a dictionary of parameters to this content type (which is empty if there are no parameters). This parameter dictionary is in the format: {: }. The final returned tuple is in the format: (, ) :rtype: tuple """ # This is the length of the (encoded) media-type and all parameters value_length = Decoder.decode_value_length(byte_iter) # Read parameters, etc, until is reached ct_field_bytes = array.array('B') for i in xrange(value_length): ct_field_bytes.append(byte_iter.next()) ct_iter = PreviewIterator(ct_field_bytes) # Now, decode all the bytes read media_type = Decoder.decode_media_type(ct_iter) # Decode the included paramaters (if any) parameters = {} while True: try: parameter, value = Decoder.decode_parameter(ct_iter) parameters[parameter] = value except StopIteration: break return media_type, parameters @staticmethod def decode_parameter(byte_iter): """ From [5], section 8.4.2.4:: Parameter = Typed-parameter | Untyped-parameter :return: The name of the parameter, and its value, in the format: (, ) :rtype: tuple """ try: return Decoder.decode_typed_parameter(byte_iter) except DecodeError: return Decoder.decode_untyped_parameter(byte_iter) @staticmethod def decode_typed_parameter(byte_iter): """ Decodes the typed parameter pointed by ``byte_iter`` The actual expected type of the value is implied by the well-known parameter. This is used in decoding parameters; see :func:`decode_parameter` From [5], section 8.4.2.4:: Typed-parameter = Well-known-parameter-token Typed-value :return: The name of the parameter, and its value, in the format: (, ) :rtype: tuple """ token, value_type = Decoder.decode_well_known_parameter(byte_iter) typed_value = '' try: typed_value = getattr(Decoder, 'decode_%s' % value_type)(byte_iter) except DecodeError, msg: raise DecodeError('Could not decode Typed-parameter: %s' % msg) except: debug('A fatal error occurred, probably due to an ' 'unimplemented decoding operation') raise return token, typed_value @staticmethod def decode_untyped_parameter(byte_iter): """ Decodes the untyped parameter pointed by ``byte_iter`` This is used in decoding parameters; see :func:`decode_parameter` The type of the value is unknown, but it shall be encoded as an integer, if that is possible. From [5], section 8.4.2.4:: Untyped-parameter = Token-text Untyped-value :return: The name of the parameter, and its value, in the format: (, ) :rtype: tuple """ token = Decoder.decode_token_text(byte_iter) value = Decoder.decode_untyped_value(byte_iter) return token, value @staticmethod def decode_untyped_value(byte_iter): """ Decodes the untyped value pointed by ``byte_iter`` This is used in decoding parameter values; see :func:`decode_untyped_parameter` From [5], section 8.4.2.4:: Untyped-value = Integer-value | Text-value :return: The decoded untyped-value :rtype: int or str """ try: return Decoder.decode_integer_value(byte_iter) except DecodeError: return Decoder.decode_text_value(byte_iter) @staticmethod def decode_well_known_parameter(byte_iter, version='1.2'): """Decodes the name and expected value type of a parameter of (for example) a "Content-Type" header entry, taking into account the WSP short form (assigned numbers) of well-known parameter names, as specified in section 8.4.2.4 and table 38 of [5]. The code values used for parameters are specified in [5], table 38 From [5], section 8.4.2.4:: Well-known-parameter-token = Integer-value :raise ValueError: The specified encoding version is invalid. :raise DecodeError: This is raised if the integer value representing the well-known parameter name cannot be decoded correctly, or the well-known paramter token value could not be found in the table of assigned content types. If this exception is raised, the iterator passed as ``byte_iter`` is not modified. :param version: The WSP encoding version to use. This defaults to "1.2", but may be "1.1", "1.2", "1.3" or 1.4" (see table 39 in [5] for details). :type version: str :return: the decoded parameter name, and its expected value type, in the format (, ) :rtype: tuple """ parameter_name = expected_value = '' try: parameter_value = Decoder.decode_integer_value(byte_iter) except DecodeError: raise DecodeError('Invalid well-known parameter token: could ' 'not read integer value representing it') wk_params = get_well_known_parameters(version) if parameter_value in wk_params: parameter_name, expected_value = wk_params[parameter_value] else: #If this is reached, the parameter isn't a WSP well-known one raise DecodeError('Invalid well-known parameter token: could ' 'not find in table of assigned numbers ' '(encoding version %s)' % version) return parameter_name, expected_value #TODO: somehow this should be more dynamic; we need to know what type # is EXPECTED (hence the TYPED value) @staticmethod def decode_typed_value(byte_iter): """ Decodes the typed value pointed by ``byte_iter`` In addition to the expected type, there may be no value. If the value cannot be encoded using the expected type, it shall be encoded as text. This is used in decoding parameters, see :func:`decode_parameter` From [5], section 8.4.2.4:: Typed-value = Compact-value | Text-value :return: The decoded Parameter Typed-value :rtype: str """ typedValue = '' try: typedValue = Decoder.decode_compact_value(byte_iter) except DecodeError: try: typedValue = Decoder.decode_text_value(byte_iter) except DecodeError: raise DecodeError('Could not decode the Parameter Typed-value') return typedValue # TODO: somehow this should be more dynamic; we need to know what # type is EXPECTED @staticmethod def decode_compact_value(byte_iter): """ Decodes the compact value pointed by ``byte_iter`` This is used in decoding parameters, see :func:`decodeTypeValue` From [5], section 8.4.2.4:: Compact-value = Integer-value | Date-value | Delta-seconds-value | Q-value | Version-value | Uri-value :raise DecodeError: Failed to decode the Parameter Compact-value; if this happens, ``byte_iter`` is unmodified :return: The decoded Compact-value (this is specific to the parameter type :rtype: str or int """ compact_value = None try: # First, see if it's an integer value # This solves the checks for: Integer-value, Date-value, # Delta-seconds-value, Q-value, Version-value compact_value = Decoder.decode_integer_value(byte_iter) except DecodeError: try: # Try parsing it as a Uri-value compact_value = Decoder.decode_uri_value(byte_iter) except DecodeError: raise DecodeError('Could not decode Parameter Compact-value') return compact_value @staticmethod def decode_date_value(byte_iter): """ Decode the data value pointed by ``byte_iter`` The encoding of dates shall be done in number of seconds from 1970-01-01, 00:00:00 GMT. From [5], section 8.4.2.3:: Date-value = Long-integer :raise DecodeError: This method uses `decode_long_integer()`, and thus raises this under the same conditions. :rtype: datetime.datetime """ return datetime.utcfromtimestamp(Decoder.decode_long_integer(byte_iter)) @staticmethod def decode_delta_seconds_value(byte_iter): """ Decodes the delta seconds value pointed by ``byte_iter`` From [5], section 8.4.2.3:: Delta-seconds-value = Integer-value :raise DecodeError: This method uses `decode_integer_value`, and thus raises this under the same conditions. :return: the decoded delta-seconds-value :rtype: int """ return Decoder.decode_integer_value(byte_iter) @staticmethod def decode_q_value(byte_iter): """ From [5], section 8.4.2.1: The encoding is the same as in uint_var-integer, but with restricted size. When quality factor 0 and quality factors with one or two decimal digits are encoded, they shall be multiplied by 100 and incremented by one, so that they encode as a one-octet value in range 1-100, ie, 0.1 is encoded as 11 (0x0B) and 0.99 encoded as 100 (0x64). Three decimal quality factors shall be multiplied with 1000 and incremented by 100, and the result shall be encoded as a one-octet or two-octet uintvar, eg, 0.333 shall be encoded as 0x83 0x31. Quality factor 1 is the default value and shall never be sent. :return: The decode quality factor (Q-value) :rtype: float """ q_value_int = Decoder.decode_uint_var(byte_iter) # TODO: limit the amount of decimal points if q_value_int > 100: return float(q_value_int - 100) / 1000.0 return float(q_value_int - 1) / 100.0 @staticmethod def decode_version_value(byte_iter): """ Decodes the version-value. From [5], section 8.4.2.3:: Version-value = Short-integer | Text-string :return: the decoded version value in the format, usually in the format: "." :rtype: str """ try: byteValue = Decoder.decode_short_integer(byte_iter) major = (byteValue & 0x70) >> 4 minor = byteValue & 0x0f return '%d.%d' % (major, minor) except DecodeError: return Decoder.decode_text_string(byte_iter) @staticmethod def decode_uri_value(byte_iter): """ Stub for Uri-value decoding; see :func:`decode_text_string` """ return Decoder.decode_text_string(byte_iter) @staticmethod def decode_text_value(byte_iter): """ Stub for Parameter Text-value decoding. This is used when decoding parameter values; see :func:`decode_typed_value` From [5], section 8.4.2.3:: Text-value = No-value | Token-text | Quoted-string :return: The decoded Parameter Text-value :rtype: str """ try: return Decoder.decode_token_text(byte_iter) except DecodeError: try: return Decoder.decode_quoted_string(byte_iter) except DecodeError: # Ok, so it's a "No-value" return '' @staticmethod def decode_no_value(byte_iter): """ Verifies that the byte pointed to by ``byte_iter`` is 0x00. If successful, this function will move ``byte_iter`` one byte forward :raise DecodeError: If 0x00 is not found; ``byte_iter`` is not modified if this is raised. :return: No-value, which is 0x00 :rtype: int """ byte_iter, local_iter = byte_iter.next() if local_iter.next() != 0x00: raise DecodeError('Expected No-value') byte_iter.next() return 0x00 @staticmethod def decode_accept_value(byte_iter): """ most of these things are currently decoded, but discarded (e.g accept-parameters); we only return the media type From [5], section 8.4.2.7:: Accept-value = Constrained-media | Accept-general-form Accept-general-form = Value-length Media-range [Accept-parameters] Media-range = (Well-known-media | Extension-Media) *(Parameter) Accept-parameters = Q-token Q-value *(Accept-extension) Accept-extension = Parameter Q-token = :raise DecodeError: The decoding failed. ``byte_iter`` will not be modified in this case. :return: the decoded Accept-value (media/content type) :rtype: str """ # Try to use Constrained-media encoding try: accept_value = Decoder.decode_constrained_media(byte_iter) except DecodeError: # ...now try Accept-general-form value_length = Decoder.decode_value_length(byte_iter) try: media = Decoder.decode_well_known_media(byte_iter) except DecodeError: media = Decoder.decode_extension_media(byte_iter) # Check for the Q-Token (to see if there are Accept-parameters) if byte_iter.preview() == 128: byte_iter.next() q_value = Decoder.decode_q_value(byte_iter) try: accept_extension = Decoder.decode_parameter(byte_iter) except DecodeError: # Just set an empty iterable accept_extension = [] byte_iter.reset_preview() accept_value = media return accept_value @staticmethod def decode_pragma_value(byte_iter): """ Decodes the pragma value pointed by ``byte_iter`` Defined in [5], section 8.4.2.38:: Pragma-value = No-cache | (Value-length Parameter) From [5], section 8.4.2.15:: No-cache = :raise DecodeError: The decoding failed. ``byte_iter`` will not be modified in this case. :return: the decoded Pragma-value, in the format: (, ) :rtype: tuple """ byte = byte_iter.preview() if byte == 0x80: # No-cache byte_iter.next() # TODO: Not sure if this parameter name (or even usage) is correct name, value = 'Cache-control', 'No-cache' else: byte_iter.reset_preview() value_length = Decoder.decode_value_length(byte_iter) name, value = Decoder.decode_parameter(byte_iter) return name, value @staticmethod def decode_well_known_charset(byte_iter): """ From [5], section 8.4.2.8:: Well-known-charset = Any-charset | Integer-value Any-charset = It is encoded using values from "Character Set Assignments" table. Equivalent to the special RFC2616 charset value "*" """ # Look for the Any-charset value byte = byte_iter.preview() byte_iter.reset_preview() if byte == 127: byte_iter.next() decoded_charset = '*' else: charset_value = Decoder.decode_integer_value(byte_iter) if charset_value in well_known_charsets: decoded_charset = well_known_charsets[charset_value] else: # This charset is not in our table... so just use the # value (at least for now) decoded_charset = str(charset_value) return decoded_charset @staticmethod def decode_well_known_header(byte_iter): """ Currently, "Wap-value" is decoded as a Text-string in most cases From [5], section 8.4.2.6:: Well-known-header = Well-known-field-name Wap-value Well-known-field-name = Short-integer Wap-value = :return: The header name, and its value, in the format: (, ) :rtype: tuple """ field_value = Decoder.decode_short_integer(byte_iter) hdr_fields = get_header_field_names() # TODO: *technically* this can fail, but then we have already # read a byte... should fix? if field_value not in xrange(len(hdr_fields)): raise DecodeError('Invalid Header Field value: %d' % field_value) field_name = hdr_fields[field_value] # TODO: make this flow better, and implement it in # decode_application_header also # Currently we decode most headers as text_strings, except # where we have a specific decoding algorithm implemented if field_name in header_field_encodings: wap_value_type = header_field_encodings[field_name] try: decoded_value = getattr(Decoder, 'decode_%s' % wap_value_type)(byte_iter) except DecodeError, msg: raise DecodeError('Could not decode Wap-value: %s' % msg) except: debug('An error occurred, probably due to an ' 'unimplemented decoding operation. Tried to ' 'decode header: %s' % field_name) raise else: decoded_value = Decoder.decode_text_string(byte_iter) return field_name, decoded_value @staticmethod def decode_application_header(byte_iter): """ From [5], section 8.4.2.6:: Application-header = Token-text Application-specific-value From [4], section 7.1:: Application-header = Token-text Application-specific-value Application-specific-value = Text-string This is used when decoding generic WSP headers; see :func:`decode_header`. We follow [4], and decode the "Application-specific-value" as a Text-string :return: The application-header, and its value, in the format: (, ) :rtype: tuple """ try: app_header = Decoder.decode_token_text(byte_iter) #FNA: added for brute-forcing except DecodeError: app_header = Decoder.decode_text_string(byte_iter) app_specific_value = Decoder.decode_text_string(byte_iter) return app_header, app_specific_value @staticmethod def decode_header(byte_iter): """ Decodes a WSP header entry "Shift-sequence" encoding has not been implemented. Currently, almost all header values are treated as text-strings From [5], section 8.4.2.6:: Header = Message-header | Shift-sequence Message-header = Well-known-header | Application-header Well-known-header = Well-known-field-name Wap-value Application-header = Token-text Application-specific-value :return: The decoded headername, and its value, in the format: (, ) :rtype: tuple """ # First try decoding the header as a well-known-header try: return Decoder.decode_well_known_header(byte_iter) except DecodeError: # ...now try Application-header encoding return Decoder.decode_application_header(byte_iter) class Encoder: """A WSP Data unit decoder""" @staticmethod def encode_uint_8(uint): """ Encodes an 8-bit unsigned integer :param uint: The integer to encode :type byte_iter: int :return: the encoded uint_8, as a sequence of bytes :rtype: list """ # Make the byte unsigned return [uint & 0xff] @staticmethod def encode_uint_var(uint): """ Variable Length Unsigned Integer encoding algorithm This binary-encodes the given unsigned integer number as specified in section 8.1.2 of [5]. Basically, each encoded byte has the following structure:: [0][ Payload ] | ^^^^^^^ | 7 bits (actual data) | Continue bit The uint is split into 7-bit segments, and the "continue bit" of each used octet is set to '1' to indicate more is to follow; the last used octet's "continue bit" is set to 0. :return: the binary-encoded uint_var, as a list of byte values :rtype: list """ uint_var = [uint & 0x7f] # Since this is the lowest entry, we do not set the continue bit to 1 uint = uint >> 7 # ...but for the remaining octets, we have to while uint > 0: uint_var.insert(0, 0x80 | (uint & 0x7f)) uint = uint >> 7 return uint_var @staticmethod def encode_text_string(string): """ Encodes a "Text-string" value. This follows the basic encoding rules specified in [5], section 8.4.2.1 :param string: The text string to encode :type string: str :return: the null-terminated, binary-encoded version of the specified Text-string, as a list of byte values :rtype: list """ encoded_string = [ord(c) for c in string] encoded_string.append(0x00) return encoded_string @staticmethod def encode_short_integer(integer): """ Encodes the specified short-integer ``integer`` value Integers in range 0-127 shall be encoded as a one octet value with the most significant bit set to one (1xxx xxxx) and with the value in the remaining least significant bits. The encoding for a long integer is specified in [5], section 8.4.2.1:: Short-integer = OCTET :param integer: The short-integer value to encode :type integer: int :raise EncodeError: Not a valid short-integer; the integer must be in the range of 0-127 :return: The encoded short integer, as a list of byte values :rtype: list """ if integer < 0 or integer > 127: raise EncodeError('Short-integer value must be in ' 'range 0-127: %d' % integer) # Make sure the MSB is set return [integer | 0x80] @staticmethod def encode_long_integer(integer): """ Encodes a Long-integer value ``integer`` The encoding for a long integer is specified in [5], section 8.4.2.1; for a description of this encoding scheme, see :func:`wsp.Decoder.decode_long_integer`. From [5], section 8.4.2.2:: Long-integer = Short-length Multi-octet-integer Short-length = :raise EncodeError: is not of type "int" :param integer: The integer value to encode :type integer: int :return: The encoded Long-integer, as a sequence of byte values :rtype: list """ if not isinstance(integer, int): raise EncodeError(' must be of type "int"') encoded_long_int = [] longInt = integer # Encode the Multi-octect-integer while longInt > 0: byte = 0xff & longInt encoded_long_int.append(byte) longInt = longInt >> 8 # Now add the SHort-length value, and make sure it's ok shortLength = len(encoded_long_int) if shortLength > 30: raise EncodeError('Cannot encode Long-integer value: Short-length ' 'is too long; should be in octet range 0-30') encoded_long_int.insert(0, shortLength) return encoded_long_int @staticmethod def encode_version_value(version): """ Encodes the version-value. Example: An MMS version of "1.0" consists of a major version of 1 and a minor version of 0, and would be encoded as 0x90. However, a version of "1.2.4" would be encoded as the Text-string "1.2.4". From [5], section 8.4.2.3:: Version-value = Short-integer | Text-string :param version: The version number to encode, e.g. "1.0" :type version: str :raise TypeError: The specified version value was not of type `str` :return: the encoded version value, as a list of byte values :rtype: list """ if not isinstance(version, str): raise TypeError('Parameter must be of type "str"') encoded_version_val = [] # First try short-integer encoding try: if len(version.split('.')) <= 2: major_version = int(version.split('.')[0]) if major_version < 1 or major_version > 7: raise ValueError('Major version must be in range 1-7') major = major_version << 4 if len(version.split('.')) == 2: minor_version = int(version.split('.')[1]) if minor_version < 0 or minor_version > 14: raise ValueError('Minor version must be in range 0-14') else: minor_version = 15 minor = minor_version encoded_version_val = Encoder.encode_short_integer(major | minor) except: # The value couldn't be encoded as a short-integer; use # a text-string instead encoded_version_val = Encoder.encode_text_string(version) return encoded_version_val @staticmethod def encode_media_type(content_type): """ Encodes the specified MIME ``content_type`` ("Media-type" value) "Well-known-media" takes into account the WSP short form of well-known content types, as specified in section 8.4.2.24 and table 40 of [5]. From [5], section 8.2.4.24:: Media-type = (Well-known-media | Extension-Media) *(Parameter) :param content_type: The MIME content type to encode :type content_type: str :return: The binary-encoded content type, as a list of (integer) byte values :rtype: list """ if content_type in well_known_content_types: # Short-integer encoding val = Encoder.encode_short_integer( well_known_content_types.index(content_type)) else: val = Encoder.encode_text_string(content_type) return [val] @staticmethod def encode_parameter(parameter_name, parameter_value, version='1.2'): """ Encodes ``parameter_name`` and ``parameter_value`` using ``version`` Binary-encodes the name of a parameter of -say- a "Content-Type" header entry, taking into account the WSP short form of well-known parameter names, as specified in section 8.4.2.4 and table 38 of [5]. From [5], section 8.4.2.4:: Parameter = Typed-parameter | Untyped-parameter Typed-parameter = Well-known-parameter-token Typed-value Untyped-parameter = Token-text Untyped-value Untyped-value = Integer-value | Text-value :param parameter_name: The name of the parameter to encode :type parameter_name: str :param parameter_value: The value of the parameter :type parameter_value: str or int :param version: The WSP encoding version to use. This defaults to "1.2", but may be "1.1", "1.2", "1.3" or "1.4" (see table 38 in [5] for details). :type version: str :raise ValueError: The specified encoding version is invalid. :return: The binary-encoded parameter name, as a list of (integer) byte values :rtype: list """ wk_params = get_well_known_parameters(version) encoded_parameter = [] # Try to encode the parameter using a "Typed-parameter" value wkParamNumbers = wk_params.keys() wkParamNumbers.sort(reverse=True) for assigned_number in wkParamNumbers: if wk_params[assigned_number][0] == parameter_name: # Ok, it's a Typed-parameter; encode the parameter name encoded_parameter.extend( Encoder.encode_short_integer(assigned_number)) # and now the value expected_type = wk_params[assigned_number][1] try: ret = getattr(Encoder, 'encode_%s' % expected_type)(parameter_value) encoded_parameter.extend(ret) except EncodeError, msg: raise EncodeError('Error encoding param value: %s' % msg) except: debug('A fatal error occurred, probably due to an ' 'unimplemented encoding operation') raise break # See if the "Typed-parameter" encoding worked if len(encoded_parameter) == 0: # it didn't. Use "Untyped-parameter" encoding encoded_parameter.extend(Encoder.encode_token_text(parameter_name)) value = [] # First try to encode the untyped-value as an integer try: value = Encoder.encode_integer_value(parameter_value) except EncodeError: value = Encoder.encode_text_string(parameter_value) encoded_parameter.extend(value) return encoded_parameter # TODO: check up on the encoding/decoding of Token-text, in particular, # how does this differ from text-string? does it have 0x00 at the end? @staticmethod def encode_token_text(text): """ From [5], section 8.4.2.1: Token-text = Token End-of-string :raise EncodeError: Specified text cannot be encoding as a token :return: The encoded token string, as a list of byte values :rtype: list """ separators = (11, 32, 40, 41, 44, 47, 58, 59, 60, 61, 62, 63, 64, 91, 92, 93, 123, 125) # Sanity check for char in separators: if chr(char) in text: raise EncodeError('Char "%s" in text string; cannot ' 'encode as Token-text' % chr(char)) return Encoder.encode_text_string(text) @staticmethod def encode_integer_value(integer): """Encodes an integer value From [5], section 8.4.2.3: Integer-Value = Short-integer | Long-integer This function will first try to encode the specified integer value into a short-integer, and failing that, will encode into a long-integer value. :param integer: The integer to encode :type integer: int :raise EncodeError: The parameter is not of type `int` :return: The encoded integer value, as a list of byte values :rtype: list """ if not isinstance(integer, int): raise EncodeError(' must be of type "int"') # First try and see if it's a short-integer try: return Encoder.encode_short_integer(integer) except EncodeError: return Encoder.encode_long_integer(integer) @staticmethod def encode_text_value(text): """Stub for encoding Text-values; see :func:`encode_text_string`""" return Encoder.encode_text_string(text) @staticmethod def encode_no_value(value=None): """ Encodes a No-value ``value``, which is 0x00 This function mainly exists for use by automatically-selected encoding routines (see :func:`encode_parameter`) for an example. :param value: This value is ignored; it is present so that this method complies with the format of the other `encode` methods. :return: A list containing a single "No-value", which is 0x00 :rtype: list """ return [0x00] @staticmethod def encode_header(field_name, value): """ Encodes a WSP header entry ``field_name``, and its ``value`` "Shift-sequence" encoding has not been implemented. Currently, almost all header values are encoded as text-strings From [5], section 8.4.2.6:: Header = Message-header | Shift-sequence Message-header = Well-known-header | Application-header Well-known-header = Well-known-field-name Wap-value Application-header = Token-text Application-specific-value :return: The encoded header, and its value, as a sequence of byte values :rtype: list """ encoded_header = [] # First try encoding the header name as a "well-known-header"... wkHdrFields = get_header_field_names() if field_name in wkHdrFields: header_field_value = Encoder.encode_short_integer( wkHdrFields.index(field_name)) encoded_header.extend(header_field_value) else: # otherwise, encode it as an "application header" encoded_header_name = Encoder.encode_token_text(field_name) encoded_header.extend(encoded_header_name) # Now add the value # TODO: make this flow better (see also Decoder.decode_header) # most header values are encoded as text_strings, except where we # have a specific Wap-value encoding implementation if field_name in header_field_encodings: wap_value_type = header_field_encodings[field_name] try: ret = getattr(Encoder, 'encode_%s' % wap_value_type)(value) encoded_header.extend(ret) except EncodeError, msg: raise EncodeError('Error encoding Wap-value: %s' % msg) except: debug('A fatal error occurred, probably due to an ' 'unimplemented encoding operation') raise else: encoded_header.extend(Encoder.encode_text_string(value)) return encoded_header @staticmethod def encode_content_type_value(media_type, parameters): """ Encodes a content type, and its parameters The short form of the Content-type-value MUST only be used when the well-known media is in the range of 0-127 or a text string. In all other cases the general form MUST be used. From [5], section 8.4.2.24:: Content-type-value = Constrained-media | Content-general-form :return: The encoded Content-type-value (including parameters, if any), as a sequence of bytes :rtype: list """ # First try do encode it using Constrained-media encoding try: if len(parameters): raise EncodeError('Need to use ' 'Content-general-form for parameters') return Encoder.encode_constrained_media(media_type) except EncodeError: # Try the general form return Encoder.encode_content_general_form(media_type, parameters) @staticmethod def encode_constrained_media(media_type): """ Encodes the constrained media ``media_type`` It is encoded using values from the "Content Type Assignments" table. From [5], section 8.4.2.7:: Constrained-media = Constrained-encoding :param media_type: The media type to encode :type media_type: str :raise EncodeError: Media value is unsuitable for Constrained-encoding :return: The encoded media type, as a sequence of bytes :rtype: list """ # See if this value is in the table of well-known content types if media_type in well_known_content_types: value = well_known_content_types.index(media_type) else: value = media_type return Encoder.encode_constrained_encoding(value) @staticmethod def encode_constrained_encoding(value): """ Constrained-encoding = Extension-Media --or-- Short-integer This encoding is used for token values, which have no well-known binary encoding, or when the assigned number of the well-known encoding is small enough to fit into Short-integer. :param value: The value to encode :type value: int or str :raise EncodeError: cannot be encoded as a Constrained-encoding sequence :return: The encoded constrained-encoding token value, as a sequence of bytes :rtype: list """ encoded_value = None if isinstance(value, int): # First try and encode the value as a short-integer encoded_value = Encoder.encode_short_integer(value) else: # Ok, it should be Extension-Media then try: encoded_value = Encoder.encode_extension_media(value) except EncodeError: # Give up raise EncodeError('Cannot encode %s as a ' 'Constrained-encoding sequence' % str(value)) return encoded_value @staticmethod def encode_extension_media(media_value): """ Encodes the extension media ``media_value`` This encoding is used for media values, which have no well-known binary encoding From [5], section 8.4.2.1:: Extension-media = *TEXT End-of-string :param media_value: The media value (string) to encode :type media_value: str :raise EncodeError: The value cannot be encoded as TEXT; probably it starts with/contains an invalid character :return: The encoded media type value, as a sequence of bytes :rtype: str """ if not isinstance(media_value, basestring): try: media_value = str(media_value) except: raise EncodeError('Invalid Extension-media: Cannot convert ' 'value to text string') char = media_value[0] if ord(char) < 32 or ord(char) == 127: raise EncodeError('Invalid Extension-media: TEXT starts with ' 'invalid character: %s' % ord(char)) return Encoder.encode_text_string(media_value) @staticmethod def encode_content_general_form(media_type, parameters): """ From [5], section 8.4.2.24:: Content-general-form = Value-length Media-type Used in decoding Content-type fields and their parameters; see :func:`decode_content_type_value`. Used by :func:`decode_content_type_value`. :return: The encoded Content-general-form, as a sequence of bytes :rtype: list """ enconded_content_general_form = [] encoded_media_type = [] # Encode the actual content type encoded_media_type = Encoder.encode_media_type(media_type) # Encode all parameters encoded_parameters = [Encoder.encode_parameter(name, parameters[name]) for name in parameters] value_length = len(encoded_media_type) + len(encoded_parameters) encoded_value_length = Encoder.encode_value_length(value_length) enconded_content_general_form.extend(encoded_value_length) enconded_content_general_form.extend(encoded_media_type) enconded_content_general_form.extend(encoded_parameters) return enconded_content_general_form @staticmethod def encode_value_length(length): """ Encodes the specified length value as a value length indicator "Value length" is used to indicate the length of a value to follow, as used in the `Content-Type` header in the MMS body, for example. The encoding for a value length indicator is specified in [5], section 8.4.2.2, and follows the form:: Value-length = [Short-length] --or-- [Length-quote] [Length] ^^^^^^ ^^^^^^ ^^^^^^ 1 byte 1 byte x bytes uint_var-integer :raise EncodeError: The value_length could not be encoded. :return: The encoded value length indicator, as a sequence of bytes :rtype: list """ encoded_value_length = [] # Try and encode it as a short-length try: encoded_value_length = Encoder.encode_short_length(length) except EncodeError: # Encode it with a Length-quote and uint_var encoded_value_length.append(31) # Length-quote encoded_value_length.extend(Encoder.encode_uint_var(length)) return encoded_value_length @staticmethod def encode_short_length(length): """ From [5], section 8.4.2.2:: Short-length = :raise EncodeError: The specified cannot be encoded as a short-length value; it is not in octet range 0-30. :return: The encoded short-length, as a sequence of bytes :rtype: list """ if length < 0 or length > 30: raise EncodeError('Cannot encode short-length; length should ' 'be in the 0-30 range') return [length] @staticmethod def encode_accept_value(accept_value): """ From [5], section 8.4.2.7:: Accept-value = Constrained-media | Accept-general-form Accept-general-form = Value-length Media-range [Accept-parameters] Media-range = (Well-known-media | Extension-Media) *(Parameter) Accept-parameters = Q-token Q-value *(Accept-extension) Accept-extension = Parameter Q-token = :note: This implementation does not currently support encoding of "Accept-parameters". :param accept_value: The Accept-value to encode (media/content type) :type accept_value: str :raise EncodeError: The encoding failed. :return: The encoded Accept-value, as a sequence of bytes :rtype: list """ encoded_accept_value = [] # Try to use Constrained-media encoding try: encoded_accept_value = Encoder.encode_constrained_media(accept_value) except EncodeError: # ...now try Accept-general-form try: encoded_media_range = Encoder.encode_media_type(accept_value) except EncodeError, msg: raise EncodeError('Cannot encode Accept-value: %s' % msg) value_length = Encoder.encode_value_length(len(encoded_media_range)) encoded_accept_value = value_length encoded_accept_value.extend(encoded_media_range) return encoded_accept_value python-messaging-0.5.12/messaging/mms/message.py0000664000175000017500000004727611707534031021433 0ustar achiangachiang# This library is free software. # # It was originally distributed under the terms of the GNU Lesser # General Public License Version 2. # # python-messaging opts to apply the terms of the ordinary GNU # General Public License v2, as permitted by section 3 of the LGPL # v2.1. This re-licensing allows the entirety of python-messaging to # be distributed according to the terms of GPL-2. # # See the COPYING file included in this archive # # The docstrings in this module contain epytext markup; API documentation # may be created by processing this file with epydoc: http://epydoc.sf.net """High-level MMS message classes""" from __future__ import with_statement import array import mimetypes import os import xml.dom.minidom class MMSMessage: """ I am an MMS message References used in this class: [1][2][3][4][5] """ def __init__(self): self._pages = [] self._data_parts = [] self._metaTags = {} self._mms_message = None self.headers = { 'Message-Type': 'm-send-req', 'Transaction-Id': '1234', 'MMS-Version': '1.0', 'Content-Type': ('application/vnd.wap.multipart.mixed', {}), } self.width = 176 self.height = 220 self.transactionID = '12345' self.subject = 'test' @property def content_type(self): """ Returns the Content-Type of this data part header No parameter information is returned; to get that, access the "Content-Type" header directly (which has a tuple value) from the message's ``headers`` attribute. This is equivalent to calling DataPart.headers['Content-Type'][0] """ return self.headers['Content-Type'][0] def add_page(self, page): """ Adds `page` to the message :type page: MMSMessagePage :param page: The message slide/page to add """ if self.content_type != 'application/vnd.wap.multipart.related': value = ('application/vnd.wap.multipart.related', {}) self.headers['Content-Type'] = value self._pages.append(page) @property def pages(self): """Returns a list of all the pages in this message""" return self._pages def add_data_part(self, data_part): """Adds a single data part (DataPart object) to the message, without connecting it to a specific slide/page in the message. A data part encapsulates some form of attachment, e.g. an image, audio etc. It is not necessary to explicitly add data parts to the message using this function if :func:`add_page` is used; this method is mainly useful if you want to create MMS messages without SMIL support, i.e. messages of type "application/vnd.wap.multipart.mixed" :param data_part: The data part to add :type data_part: DataPart """ self._data_parts.append(data_part) @property def data_parts(self): """ Returns a list of all the data parts in this message including data parts that were added to slides in this message""" parts = [] if len(self._pages): parts.append(self.smil()) for slide in self._mms_message._pages: parts.extend(slide.data_parts()) parts.extend(self._data_parts) return parts def smil(self): """Returns the text of the message's SMIL file""" impl = xml.dom.minidom.getDOMImplementation() smil_doc = impl.createDocument(None, "smil", None) # Create the SMIL header head_node = smil_doc.createElement('head') # Add metadata to header for tag_name in self._metaTags: meta_node = smil_doc.createElement('meta') meta_node.setAttribute(tag_name, self._metaTags[tag_name]) head_node.appendChild(meta_node) # Add layout info to header layout_node = smil_doc.createElement('layout') root_layout_node = smil_doc.createElement('root-layout') root_layout_node.setAttribute('width', str(self.width)) root_layout_node.setAttribute('height', str(self.height)) layout_node.appendChild(root_layout_node) areas = (('Image', '0', '0', '176', '144'), ('Text', '176', '144', '176', '76')) for region_id, left, top, width, height in areas: region_node = smil_doc.createElement('region') region_node.setAttribute('id', region_id) region_node.setAttribute('left', left) region_node.setAttribute('top', top) region_node.setAttribute('width', width) region_node.setAttribute('height', height) layout_node.appendChild(region_node) head_node.appendChild(layout_node) smil_doc.documentElement.appendChild(head_node) # Create the SMIL body body_node = smil_doc.createElement('body') # Add pages to body for page in self._pages: par_node = smil_doc.createElement('par') par_node.setAttribute('duration', str(page.duration)) # Add the page content information if page.image is not None: #TODO: catch unpack exception part, begin, end = page.image if 'Content-Location' in part.headers: src = part.headers['Content-Location'] elif 'Content-ID' in part.headers: src = part.headers['Content-ID'] else: src = part.data image_node = smil_doc.createElement('img') image_node.setAttribute('src', src) image_node.setAttribute('region', 'Image') if begin > 0 or end > 0: if end > page.duration: end = page.duration image_node.setAttribute('begin', str(begin)) image_node.setAttribute('end', str(end)) par_node.appendChild(image_node) if page.text is not None: part, begin, end = page.text src = part.data text_node = smil_doc.createElement('text') text_node.setAttribute('src', src) text_node.setAttribute('region', 'Text') if begin > 0 or end > 0: if end > page.duration: end = page.duration text_node.setAttribute('begin', str(begin)) text_node.setAttribute('end', str(end)) par_node.appendChild(text_node) if page.audio is not None: part, begin, end = page.audio if 'Content-Location' in part.headers: src = part.headers['Content-Location'] elif 'Content-ID' in part.headers: src = part.headers['Content-ID'] else: src = part.data audio_node = smil_doc.createElement('audio') audio_node.setAttribute('src', src) if begin > 0 or end > 0: if end > page.duration: end = page.duration audio_node.setAttribute('begin', str(begin)) audio_node.setAttribute('end', str(end)) par_node.appendChild(text_node) par_node.appendChild(audio_node) body_node.appendChild(par_node) smil_doc.documentElement.appendChild(body_node) return smil_doc.documentElement.toprettyxml() def encode(self): """ Return a binary representation of this MMS message This uses the `~:class:messaging.mms.mms_pdu.MMSEncoder` internally :return: The binary-encoded MMS data, as an array of bytes :rtype: array.array('B') """ from messaging.mms import mms_pdu encoder = mms_pdu.MMSEncoder() return encoder.encode(self) def to_file(self, filename): """ Writes this MMS message to `filename` in binary-encoded form This uses the `~:class:messaging.mms.mms_pdu.MMSEncoder` internally :param filename: The path where to store the message data :type filename: str :rtype array.array('B') :return: The binary-encode MMS data, as an array of bytes """ with open(filename, 'wb') as f: self.encode().tofile(f) @staticmethod def from_data(data): """ Returns a new `:class:MMSMessage` out of ``data`` This uses the `~:class:messaging.mms.mms_pdu.MMSEncoder` internally :param data: The data to load :type filename: array.array """ from messaging.mms import mms_pdu decoder = mms_pdu.MMSDecoder() return decoder.decode_data(data) @staticmethod def from_file(filename): """ Returns a new `:class:MMSMessage` out of file ``filename`` This uses the `~:class:messaging.mms.mms_pdu.MMSEncoder` internally :param filename: The name of the file to load :type filename: str """ from messaging.mms import mms_pdu decoder = mms_pdu.MMSDecoder() return decoder.decode_file(filename) class MMSMessagePage: """ A single page/slide in an MMS Message. In order to ensure that the MMS message can be correctly displayed by most terminals, each page's content is limited to having 1 image, 1 audio clip and 1 block of text, as stated in [1]. The default slide duration is set to 4 seconds; use :func:`set_duration` to change this. """ def __init__(self): self.duration = 4000 self.image = None self.audio = None self.text = None @property def data_parts(self): """Returns a list of the data parst in this slide""" return [part for part in (self.image, self.audio, self.text) if part is not None] def number_of_parts(self): """ Returns the number of data parts in this slide @rtype: int """ num_parts = 0 for item in (self.image, self.audio, self.text): if item is not None: num_parts += 1 return num_parts #TODO: find out what the "ref" element in SMIL does #TODO: add support for "alt" element; also make sure what it does def add_image(self, filename, time_begin=0, time_end=0): """ Adds an image to this slide. :param filename: The name of the image file to add. Supported formats are JPEG, GIF and WBMP. :type filename: str :param time_begin: The time (in milliseconds) during the duration of this slide to begin displaying the image. If this is 0 or less, the image will be displayed from the moment the slide is opened. :type time_begin: int :param time_end: The time (in milliseconds) during the duration of this slide at which to stop showing (i.e. hide) the image. If this is 0 or less, or if it is greater than the actual duration of this slide, it will be shown until the next slide is accessed. :type time_end: int :raise TypeError: An inappropriate variable type was passed in of the parameters """ if not isinstance(filename, str): raise TypeError("filename must be a string") if not isinstance(time_begin, int) or not isinstance(time_end, int): raise TypeError("time_begin and time_end must be ints") if not os.path.isfile(filename): raise OSError("filename must be a file") if time_end > 0 and time_end < time_begin: raise ValueError('time_end cannot be lower than time_begin') self.image = (DataPart(filename), time_begin, time_end) def add_audio(self, filename, time_begin=0, time_end=0): """ Adds an audio clip to this slide. :param filename: The name of the audio file to add. Currently the only supported format is AMR. :type filename: str :param time_begin: The time (in milliseconds) during the duration of this slide to begin playback of the audio clip. If this is 0 or less, the audio clip will be played the moment the slide is opened. :type time_begin: int :param time_end: The time (in milliseconds) during the duration of this slide at which to stop playing (i.e. mute) the audio clip. If this is 0 or less, or if it is greater than the actual duration of this slide, the entire audio clip will be played, or until the next slide is accessed. :type time_end: int :raise TypeError: An inappropriate variable type was passed in of the parameters """ if not isinstance(filename, str): raise TypeError("filename must be a string") if not isinstance(time_begin, int) or not isinstance(time_end, int): raise TypeError("time_begin and time_end must be ints") if not os.path.isfile(filename): raise OSError("filename must be a file") if time_end > 0 and time_end < time_begin: raise ValueError('time_end cannot be lower than time_begin') self.audio = (DataPart(filename), time_begin, time_end) def add_text(self, text, time_begin=0, time_end=0): """ Adds a block of text to this slide. :param text: The text to add to the slide. :type text: str :param time_begin: The time (in milliseconds) during the duration of this slide to begin displaying the text. If this is 0 or less, the text will be displayed from the moment the slide is opened. :type time_begin: int :param time_end: The time (in milliseconds) during the duration of this slide at which to stop showing (i.e. hide) the text. If this is 0 or less, or if it is greater than the actual duration of this slide, it will be shown until the next slide is accessed. :type time_end: int :raise TypeError: An inappropriate variable type was passed in of the parameters """ if not isinstance(text, str): raise TypeError("Text must be a string") if not isinstance(time_begin, int) or not isinstance(time_end, int): raise TypeError("time_begin and time_end must be ints") if time_end > 0 and time_end < time_begin: raise ValueError('time_end cannot be lower than time_begin') time_data = DataPart() time_data.set_text(text) self.text = (time_data, time_begin, time_end) def set_duration(self, duration): """ Sets the maximum duration of this slide (i.e. how long this slide should be displayed) @param duration: the maxium slide duration, in milliseconds @type duration: int @raise TypeError: must be an integer @raise ValueError: the requested duration is invalid (must be a non-zero, positive integer) """ if not isinstance(duration, int): raise TypeError("Duration must be an int") if duration < 1: raise ValueError('duration may not be 0 or negative') self.duration = duration class DataPart(object): """ I am a data entry in the MMS body A DataPart object encapsulates any data content that is to be added to the MMS (e.g. an image , raw image data, audio clips, text, etc). A DataPart object can be queried using the Python built-in :func:`len` function. This encapsulation allows custom header/parameter information to be set for each data entry in the MMS. Refer to [5] for more information on these. """ def __init__(self, filename=None): """ @param srcFilename: If specified, load the content of the file with this name @type srcFilename: str """ super(DataPart, self).__init__() self.content_type_parameters = {} self.headers = {'Content-Type': ('application/octet-stream', {})} self._filename = None self._data = None if filename is not None: self.from_file(filename) def _get_content_type(self): """ Returns the string representation of this data part's "Content-Type" header. No parameter information is returned; to get that, access the "Content-Type" header directly (which has a tuple value)from this part's C{headers} attribute. This is equivalent to calling DataPart.headers['Content-Type'][0] """ return self.headers['Content-Type'][0] def _set_content_type(self, value): """Sets the content type string, with no parameters """ self.headers['Content-Type'] = value, {} content_type = property(_get_content_type, _set_content_type) def from_file(self, filename): """ Load the data contained in the specified file This function clears any previously-set header entries. :param filename: The name of the file to open :type filename: str :raises OSError: The filename is invalid """ if not os.path.isfile(filename): raise OSError('The file "%s" does not exist' % filename) # Clear any headers that are currently set self.headers = {} self._data = None self.headers['Content-Location'] = os.path.basename(filename) content_type = (mimetypes.guess_type(filename)[0] or 'application/octet-stream', {}) self.headers['Content-Type'] = content_type self._filename = filename def set_data(self, data, content_type, ct_parameters=None): """ Explicitly set the data contained by this part This function clears any previously-set header entries. :param data: The data to hold :type data: str :param content_type: The MIME content type of the specified data :type content_type: str :param ct_parameters: Any content type header paramaters to add :type ct_parameters: dict """ self.headers = {} self._filename = None self._data = data if ct_parameters is None: ct_parameters = {} self.headers['Content-Type'] = content_type, ct_parameters def set_text(self, text): """ Convenience wrapper method for set_data() This method sets the :class:`DataPart` object to hold the specified text string, with MIME content type "text/plain". @param text: The text to hold @type text: str """ self.set_data(text, 'text/plain') def __len__(self): """Provides the length of the data encapsulated by this object""" if self._filename is not None: return int(os.stat(self._filename)[6]) else: return len(self.data) @property def data(self): """A buffer containing the binary data of this part""" if self._data is not None: if type(self._data) == array.array: self._data = self._data.tostring() return self._data elif self._filename is not None: with open(self._filename, 'r') as f: self._data = f.read() return self._data return '' python-messaging-0.5.12/messaging/__init__.py0000664000175000017500000000004411707534031020730 0ustar achiangachiang# see LICENSE VERSION = (0, 5, 12) python-messaging-0.5.12/messaging/utils.py0000664000175000017500000001377211707534031020345 0ustar achiangachiangfrom array import array from datetime import timedelta, tzinfo from math import floor import sys class FixedOffset(tzinfo): """Fixed offset in minutes east from UTC.""" def __init__(self, offset, name): if isinstance(offset, timedelta): self.offset = offset else: self.offset = timedelta(minutes=offset) self.__name = name @classmethod def from_timezone(cls, tz_str, name): # no timezone, GMT+3, GMT-3 # '', '+0330', '-0300' if not tz_str: return cls(timedelta(0), name) sign = 1 if '+' in tz_str else -1 offset = tz_str.replace('+', '').replace('-', '') hours, minutes = int(offset[:2]), int(offset[2:]) minutes += hours * 60 if sign == 1: td = timedelta(minutes=minutes) elif sign == -1: td = timedelta(days=-1, minutes=minutes) return cls(td, name) def utcoffset(self, dt): return self.offset def tzname(self, dt): return self.__name def dst(self, dt): return timedelta(0) def bytes_to_str(b): if sys.version_info >= (3,): return b.decode('latin1') return b def to_array(pdu): return array('B', [int(pdu[i:i + 2], 16) for i in range(0, len(pdu), 2)]) def to_bytes(s): if sys.version_info >= (3,): return bytes(s) return ''.join(map(unichr, s)) def debug(s): # set this to True if you want to poke at PDU encoding/decoding if False: print s def swap(s): """Swaps ``s`` according to GSM 23.040""" what = s[:] for n in range(1, len(what), 2): what[n - 1], what[n] = what[n], what[n - 1] return what def swap_number(n): data = swap(list(n.replace('f', ''))) return ''.join(data) def clean_number(n): return n.strip().replace(' ', '') def encode_str(s): """Returns the hexadecimal representation of ``s``""" return ''.join(["%02x" % ord(n) for n in s]) def encode_bytes(b): return ''.join(["%02x" % n for n in b]) def pack_8bits_to_7bits(message, udh=None): pdu = "" txt = bytes_to_str(message) if udh is None: tl = len(txt) txt += '\x00' msgl = int(len(txt) * 7 / 8) op = [-1] * msgl c = shift = 0 for n in range(msgl): if shift == 6: c += 1 shift = n % 7 lb = ord(txt[c]) >> shift hb = (ord(txt[c + 1]) << (7 - shift) & 255) op[n] = lb + hb c += 1 pdu = chr(tl) + ''.join(map(chr, op)) else: txt = "\x00\x00\x00\x00\x00\x00" + txt tl = len(txt) txt += '\x00' msgl = int(len(txt) * 7 / 8) op = [-1] * msgl c = shift = 0 for n in range(msgl): if shift == 6: c += 1 shift = n % 7 lb = ord(txt[c]) >> shift hb = (ord(txt[c + 1]) << (7 - shift) & 255) op[n] = lb + hb c += 1 for i, char in enumerate(udh): op[i] = ord(char) pdu = chr(tl) + ''.join(map(chr, op)) return encode_str(pdu) def pack_8bits_to_8bit(message, udh=None): text = message if udh is not None: text = udh + text mlen = len(text) message = chr(mlen) + message return encode_str(message) def pack_8bits_to_ucs2(message, udh=None): # XXX: This does not control the size respect to UDH text = message nmesg = '' if udh is not None: text = udh + text for n in text: nmesg += chr(ord(n) >> 8) + chr(ord(n) & 0xFF) mlen = len(text) * 2 message = chr(mlen) + nmesg return encode_str(message) def unpack_msg(pdu): """Unpacks ``pdu`` into septets and returns the decoded string""" # Taken/modified from Dave Berkeley's pysms package count = last = 0 result = [] for i in range(0, len(pdu), 2): byte = int(pdu[i:i + 2], 16) mask = 0x7F >> count out = ((byte & mask) << count) + last last = byte >> (7 - count) result.append(out) if len(result) >= 0xa0: break if count == 6: result.append(last) last = 0 count = (count + 1) % 7 return to_bytes(result) def unpack_msg2(pdu): """Unpacks ``pdu`` into septets and returns the decoded string""" # Taken/modified from Dave Berkeley's pysms package count = last = 0 result = [] for byte in pdu: mask = 0x7F >> count out = ((byte & mask) << count) + last last = byte >> (7 - count) result.append(out) if len(result) >= 0xa0: break if count == 6: result.append(last) last = 0 count = (count + 1) % 7 return to_bytes(result) def timedelta_to_relative_validity(t): """ Convert ``t`` to its relative validity period In case the resolution of ``t`` is too small for a time unit, it will be floor-rounded to the previous sane value :type t: datetime.timedelta :return int """ if t < timedelta(minutes=5): raise ValueError("Min resolution is five minutes") if t > timedelta(weeks=63): raise ValueError("Max validity is 63 weeks") if t <= timedelta(hours=12): return floor(t.seconds / (60 * 5)) - 1 if t <= timedelta(hours=24): t -= timedelta(hours=12) return floor(t.seconds / (60 * 30)) + 143 if t <= timedelta(days=30): return t.days + 166 if t <= timedelta(weeks=63): return floor(t.days / 7) + 192 def datetime_to_absolute_validity(d, tzname='Unknown'): """Convert ``d`` to its integer representation""" n = d.strftime("%y %m %d %H %M %S %z").split(" ") # compute offset offset = FixedOffset.from_timezone(n[-1], tzname).offset # one unit is 15 minutes s = "%02d" % int(floor(offset.seconds / (60 * 15))) if offset.days < 0: # set MSB to 1 s = "%02x" % ((int(s[0]) << 4) | int(s[1]) | 0x80) n[-1] = s return [int(c[::-1], 16) for c in n] python-messaging-0.5.12/doc/0000775000175000017500000000000011707534031015411 5ustar achiangachiangpython-messaging-0.5.12/doc/modules/0000775000175000017500000000000011707534031017061 5ustar achiangachiangpython-messaging-0.5.12/doc/modules/sms/0000775000175000017500000000000011707534031017663 5ustar achiangachiangpython-messaging-0.5.12/doc/modules/sms/deliver.rst0000664000175000017500000000026111707534031022046 0ustar achiangachiang:mod:`messaging.sms.deliver` ============================ .. automodule:: messaging.sms.deliver Classes -------- .. autoclass:: SmsDeliver :show-inheritance: :members: python-messaging-0.5.12/doc/modules/sms/submit.rst0000664000175000017500000000025511707534031021722 0ustar achiangachiang:mod:`messaging.sms.submit` =========================== .. automodule:: messaging.sms.submit Classes -------- .. autoclass:: SmsSubmit :show-inheritance: :members: python-messaging-0.5.12/doc/modules/sms/udh.rst0000664000175000017500000000035411707534031021177 0ustar achiangachiang:mod:`messaging.sms.udh` ======================== .. automodule:: messaging.sms.udh Classes -------- .. autoclass:: PortAddress :members: .. autoclass:: ConcatReference :members: .. autoclass:: UserDataHeader :members: python-messaging-0.5.12/doc/modules/sms/wap.rst0000664000175000017500000000030511707534031021202 0ustar achiangachiang:mod:`messaging.sms.wap` ======================== .. automodule:: messaging.sms.wap Functions --------- .. autofunction:: is_a_wap_push_notification .. autofunction:: extract_push_notification python-messaging-0.5.12/doc/modules/sms/base.rst0000664000175000017500000000021711707534031021327 0ustar achiangachiang:mod:`messaging.sms.base` ========================= .. automodule:: messaging.sms.base Classes -------- .. autoclass:: SmsBase :members: python-messaging-0.5.12/doc/modules/sms/pdu.rst0000664000175000017500000000021211707534031021200 0ustar achiangachiang:mod:`messaging.sms.pdu` ========================== .. automodule:: messaging.sms.pdu Classes -------- .. autoclass:: Pdu :members: python-messaging-0.5.12/doc/modules/sms/gsm0338.rst0000664000175000017500000000022511707534031021520 0ustar achiangachiang:mod:`messaging.sms.gsm0338` ============================ .. automodule:: messaging.sms.gsm0338 Functions --------- .. autofunction:: is_gsm_text python-messaging-0.5.12/doc/modules/mms/0000775000175000017500000000000011707534031017655 5ustar achiangachiangpython-messaging-0.5.12/doc/modules/mms/message.rst0000664000175000017500000000035511707534031022036 0ustar achiangachiang:mod:`messaging.mms.message` ============================ .. automodule:: messaging.mms.message Classes -------- .. autoclass:: MMSMessage :members: .. autoclass:: MMSMessagePage :members: .. autoclass:: DataPart :members: python-messaging-0.5.12/doc/modules/mms/wsp_pdu.rst0000664000175000017500000000061511707534031022072 0ustar achiangachiang:mod:`messaging.mms.wsp_pdu` ============================ .. automodule:: messaging.mms.wsp_pdu Functions --------- .. autofunction:: get_header_field_names .. autofunction:: get_well_known_parameters Classes -------- .. autoclass:: DecodeError :show-inheritance: .. autoclass:: EncodeError :show-inheritance: .. autoclass:: Decoder :members: .. autoclass:: Encoder :members: python-messaging-0.5.12/doc/modules/mms/iterator.rst0000664000175000017500000000024311707534031022237 0ustar achiangachiang:mod:`messaging.mms.iterator` ============================= .. automodule:: messaging.mms.iterator Classes -------- .. autoclass:: PreviewIterator :members: python-messaging-0.5.12/doc/modules/mms/mms_pdu.rst0000664000175000017500000000035711707534031022060 0ustar achiangachiang:mod:`messaging.mms.mms_pdu` ============================ .. automodule:: messaging.mms.mms_pdu Classes -------- .. autoclass:: MMSEncoder :show-inheritance: :members: .. autoclass:: MMSDecoder :show-inheritance: :members: python-messaging-0.5.12/doc/modules/utils.rst0000664000175000017500000000114511707534031020754 0ustar achiangachiang:mod:`messaging.utils` ========================= .. automodule:: messaging.utils Classes ------- .. autoclass:: FixedOffset :members: Functions --------- .. autofunction:: bytes_to_str .. autofunction:: to_array .. autofunction:: to_bytes .. autofunction:: swap .. autofunction:: swap_number .. autofunction:: clean_number .. autofunction:: encode_str .. autofunction:: pack_8bits_to_7bits .. autofunction:: pack_8bits_to_8bit .. autofunction:: pack_8bits_to_ucs2 .. autofunction:: unpack_msg .. autofunction:: timedelta_to_relative_validity .. autofunction:: datetime_to_absolute_validity python-messaging-0.5.12/doc/index.rst0000664000175000017500000000056311707534031017256 0ustar achiangachiangWelcome to python-messaging's documentation! ============================================ Contents: .. toctree:: :maxdepth: 2 tutorial/sms tutorial/mms glossary Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`glossary` * :ref:`search` .. toctree:: :hidden: :glob: modules/* modules/sms/* modules/mms/* python-messaging-0.5.12/doc/glossary.rst0000664000175000017500000000244411707534031020012 0ustar achiangachiang.. _glossary: ======== Glossary ======== .. glossary:: :sorted: MMS Multimedia Messaging Service, or MMS, is a standard way to send messages that include multimedia content to and from mobile phones. It extends the core SMS (Short Message Service) capability which only allowed exchange of text messages up to 160 characters in length. MMSC A "store and forward" server for MMS. If the recipient is in another network, the MMS will be forwarded to the recipient's carrier using the Internet. SMS Short Message Service (SMS) is the text communication service component of phone, web or mobile communication systems, using standardized communications protocols that allow the exchange of short text messages (up to 160 characters) between fixed line or mobile phone devices. WAP Wireless Application Protocol (WAP) is an open international standard for application-layer network communications in a wireless-communication environment. Most use of WAP involves accessing the mobile web from a mobile phone or from a PDA. PDU A PDU is a sequence of bytes used in telecommunications to convey information from one host/device to another. python-messaging-0.5.12/doc/tutorial/0000775000175000017500000000000011707534031017254 5ustar achiangachiangpython-messaging-0.5.12/doc/tutorial/mms.rst0000664000175000017500000001171311707534031020605 0ustar achiangachiang============ MMS tutorial ============ Features ======== * Full featured MMS encoder/decoder * Supports MMS 1.0-1.4 * Supports presentation layout * Handles well known formats: JPEG, GIF, AMR, MIDI, 3GP, etc. * Tested with WAP 2.0 gateways Encoding ======== How to encode a MMS:: from messaging.mms.message import MMSMessage, MMSMessagePage mms = MMSMessage() mms.headers['To'] = '+34231342234/TYPE=PLMN' mms.headers['Message-Type'] = 'm-send-req' mms.headers['Subject'] = 'Test python-messaging.mms' slide1 = MMSMessagePage() slide1.add_image('image1.jpg') slide1.add_text('This is the first slide, with a static image and some text.') slide2 = MMSMessagePage() slide2.set_duration(4500) slide2.add_image('image2.jpg', 1500) slide2.add_text('This second slide has some timing effects.', 500, 3500) mms.add_page(slide1) mms.add_page(slide2) payload = mms.encode() The above snippet binary encodes a MMS for '+34231342234' and subject 'Test python-messaging.mms' with two slides. The first slide is just an static image with some text, the second one has timing effects and will last 4.5s. Sending a MMS +++++++++++++ In a WAP2.0 gateway, the binary message (payload) will be used as an argument for a plain HTTP POST:: from cStringIO import StringIO import socket gw_host, gw_port = "212.11.23.23", 7899 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((gw_host, gw_port)) s.send("POST %s HTTP/1.0\r\n" % mmsc) s.send("Content-Type: application/vnd.wap.mms-message\r\n") s.send("Content-Length: %d\r\n\r\n" % len(payload)) s.sendall(payload) buf = StringIO() while True: data = s.recv(4096) if not data: break buf.write(data) s.close() data = buf.getvalue() buf.close() print "PROXY RESPONSE", data Encoding a m-notifyresp-ind PDU +++++++++++++++++++++++++++++++ In order to send a m-notifyresp-ind, you will need to know the transaction id of the MMS you want to acknowledge, once you have that you just need to:: mms = MMSMessage() mms.headers['Transaction-Id'] = 'T2132112322' mms.headers['Message-Type'] = 'm-notifyresp-ind' mms.headers['Status'] = 'Retrieved' payload = mms.encode() And POST the resulting payload to the :term:`MMSC` proxy using the very same code used for sending a MMS. Decoding ======== Decoding from binary data +++++++++++++++++++++++++ Decoding a MMS could not be any easier, once you have the binary data of the MMS, you just need to:: from messaging.mms.message import MMSMessage # data is an array.array("B") instance mms = MMSMessage.from_data(data) print mms.headers['Message-Type'] # m-send-req print mms.headers['To'] # '+34231342234/TYPE=PLMN' Decoding from a file ++++++++++++++++++++ Decoding a MMS serialised to a file is pretty straightforward too, you just need the path to the file and:: from messaging.mms.message import MMSMessage path = '/tmp/binary-mms.bin' mms = MMSMessage.from_file(path) print mms.headers['Message-Type'] # m-send-req print mms.headers['To'] # '+34231342234/TYPE=PLMN' Obtaining a MMS from a WAP push notification ++++++++++++++++++++++++++++++++++++++++++++ A WAP push notification conveys all the necessary information to retrieve the MMS from the MMSC. Once you have the different PDUs of the WAP push, you need to decode it and obtain the `Content-Location` value of the headers:: from messaging.sms import SmsDeliver from messaging.sms.wap import extract_push_notification pdus = [ "0791447758100650400E80885810000000810004016082415464408C0C08049F8E020105040B8423F00106226170706C69636174696F6E2F766E642E7761702E6D6D732D6D65737361676500AF848C82984E4F4B3543694B636F544D595347344D4253774141734B7631344655484141414141414141008D908919802B3434373738353334323734392F545950453D504C4D4E008A808E0274008805810301194083687474703A2F", "0791447758100650440E8088581000000081000401608241547440440C08049F8E020205040B8423F02F70726F6D6D732F736572766C6574732F4E4F4B3543694B636F544D595347344D4253774141734B763134465548414141414141414100", ] data = "" sms = SmsDeliver(pdus[0]) data += sms.text sms = SmsDeliver(pdus[1]) data += sms.text mms = extract_push_notification(data) url = mms.headers['Content-Location'] print url Once you have the content location, you need to do a HTTP GET to retrieve the MMS payload:: import socket from cStringIO import StringIO from messaging.mms.message import MMSMessage gw_host, gw_port = "212.11.23.23", 7899 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((gw_host, gw_port)) s.send("GET %s HTTP/1.0\r\n\r\n" % url) buf = StringIO() while True: data = s.recv(4096) if not data: break buf.write(data) s.close() data = buf.getvalue() buf.close() mms = MMSMessage.from_data(data) print mms python-messaging-0.5.12/doc/tutorial/sms.rst0000664000175000017500000000751711707534031020622 0ustar achiangachiang============ SMS tutorial ============ Features ++++++++ python-messaging contains in :mod:`messaging.sms` a full featured :term:`SMS` encoder/decoder that should fulfill your needs. Some of its features: - 7bit/8bit/16bit encoding/decoding: Decode messages from your Chinese friends, decode a :term:`WAP` PUSH notification or encode a UCS2 message with a `Haiku`_ for your Japanese relatives. - Encode/decode `concatenated SMS`_ - Set SMS validity: (relative, absolute) - Set SMS class: (0-3) - Encode/decode read reports - Decode alphanumeric senders - Pythonic API: We have strived to design an API that will make feel Pythonistas right at home! .. _Haiku: http://en.wikipedia.org/wiki/Haiku .. _concatenated SMS: http://en.wikipedia.org/wiki/Concatenated_SMS Encoding ++++++++ Single part vs Multipart ~~~~~~~~~~~~~~~~~~~~~~~~ How to encode a single part SMS ready to be sent:: from messaging.sms import SmsSubmit sms = SmsSubmit("+44123231231", "hey how's it going?") pdu = sms.to_pdu()[0] print pdu.length, pdu.pdu How to encode a concatenated SMS ready to be sent:: from messaging.sms import SmsSubmit sms = SmsSubmit("+44123231231", "hey " * 50) for pdu in sms.to_pdu(): print pdu.length, pdu.pdu Setting class ~~~~~~~~~~~~~ Setting the SMS class (0-3) is a no brainer:: from messaging.sms import SmsSubmit sms = SmsSubmit("+44123231231", "hey how's it going?") sms.class = 0 pdu = sms.to_pdu()[0] print pdu.length, pdu.pdu Setting validity ~~~~~~~~~~~~~~~~ Validity can be either absolute, or relative. In order to provide a pythonic API, we are using :class:`datetime.datetime` and :class:`datetime.timedelta` objects respectively. Setting absolute validity:: from datetime import datetime from messaging.sms import SmsSubmit sms = SmsSubmit("+44123231231", "this SMS will auto-destroy in 4 months) sms.validity = datetime(2010, 12, 31, 23, 59, 59) pdu = sms.to_pdu()[0] print pdu.length, pdu.pdu Setting relative validity:: from datetime import timedelta from messaging.sms import SmsSubmit sms = SmsSubmit("+44123231231", "this SMS will auto-destroy in 5 hours") sms.validity = timedelta(hours=5) pdu = sms.to_pdu()[0] print pdu.length, pdu.pdu Decoding ++++++++ term:`PDU` decoding is really simple with :class:`~messaging.sms.SmsDeliver`:: from messaging.sms import SmsDeliver pdu = "0791447758100650040C914497726247010000909010711423400A2050EC468B81C4733A" sms = SmsDeliver(pdu) print sms.data # {'csca': '+447785016005', 'type': None, # 'date': datetime.datetime(2009, 9, 1, 16, 41, 32), # 'text': u' 1741 bst', 'fmt': 0, 'pid': 0, # 'dcs': 0, 'number': '+447927267410'} Apart from the pdu, the :py:meth:`messaging.sms.SmsDeliver.__init__` accepts a second parameter (`strict`, which defaults to True). If False, it will decode incomplete (odd size) PDUs. Sending +++++++ This is how you would send a SMS with a modem or a 3G device on Linux, the following code assumes that the device is already authenticated and registered:: import serial from messaging.sms import SmsSubmit def send_text(number, text, path='/dev/ttyUSB0'): # encode the SMS # note how I get the first returned element, this does # not deal with concatenated SMS. pdu = SmsSubmit(number, text).to_pdu()[0] # open the modem port (assumes Linux) ser = serial.Serial(path, timeout=1) # write the PDU length and wait 1 second till the # prompt appears (a more robust implementation # would wait till the prompt appeared) ser.write('AT+CMGS=%d\r' % pdu.length) print ser.readlines() # write the PDU and send a Ctrl+z escape ser.write('%s\x1a' % pdu.pdu) ser.close() send_text('655234567', 'hey how are you?') python-messaging-0.5.12/doc/Makefile0000664000175000017500000001102611707534031017051 0ustar achiangachiang# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/python-messaging.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/python-messaging.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/python-messaging" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/python-messaging" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." make -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." python-messaging-0.5.12/doc/conf.py0000664000175000017500000001350511707534031016714 0ustar achiangachiang# -*- coding: utf-8 -*- # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.coverage', 'sphinx.ext.viewcode'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'python-messaging' copyright = u'2010, Pablo Martí' # The short X.Y version. version = '0.5.9' # The full version, including alpha/beta/rc tags. release = '0.5.9' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). add_module_names = False # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'default' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'python-messagingdoc' # -- Options for LaTeX output -------------------------------------------------- # The paper size ('letter' or 'a4'). #latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). #latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'python-messaging.tex', u'python-messaging Documentation', u'Pablo Martí', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Additional stuff for the LaTeX preamble. #latex_preamble = '' # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'python-messaging', u'python-messaging Documentation', [u'Pablo Martí'], 1) ] python-messaging-0.5.12/LICENSE0000664000175000017500000000065111707534031015653 0ustar achiangachiangCopyright (C) 2004 Paul Hardwick Copyright (C) 2008 Warp Networks S.L. Copyright (C) 2008 Telefonica I+D Copyright (C) 2008 Francois Aucamp Imported for the wader project on 5 June 2008 by Pablo Martí Imported for the mobile-manager on 1 Oct 2008 by Roberto Majadas Copyright (C) 2008-2010 python-messaging developers See the COPYING file for the gory details of the GPLv2. python-messaging-0.5.12/distribute_setup.py0000644000175000017500000003566512041140736020626 0ustar achiangachiang#!python """Bootstrap distribute installation If you want to use setuptools in your package's setup.py, just include this file in the same directory with it, and add this to the top of your setup.py:: from distribute_setup import use_setuptools use_setuptools() If you want to require a specific version of setuptools, set a download mirror, or use an alternate download directory, you can do so by supplying the appropriate options to ``use_setuptools()``. This file can also be run as a script to install or upgrade setuptools. """ import os import sys import time import fnmatch import tempfile import tarfile from distutils import log try: from site import USER_SITE except ImportError: USER_SITE = None try: import subprocess def _python_cmd(*args): args = (sys.executable,) + args return subprocess.call(args) == 0 except ImportError: # will be used for python 2.3 def _python_cmd(*args): args = (sys.executable,) + args # quoting arguments if windows if sys.platform == 'win32': def quote(arg): if ' ' in arg: return '"%s"' % arg return arg args = [quote(arg) for arg in args] return os.spawnl(os.P_WAIT, sys.executable, *args) == 0 DEFAULT_VERSION = "0.6.10" DEFAULT_URL = "http://pypi.python.org/packages/source/d/distribute/" SETUPTOOLS_FAKED_VERSION = "0.6c11" SETUPTOOLS_PKG_INFO = """\ Metadata-Version: 1.0 Name: setuptools Version: %s Summary: xxxx Home-page: xxx Author: xxx Author-email: xxx License: xxx Description: xxx """ % SETUPTOOLS_FAKED_VERSION def _install(tarball): # extracting the tarball tmpdir = tempfile.mkdtemp() log.warn('Extracting in %s', tmpdir) old_wd = os.getcwd() try: os.chdir(tmpdir) tar = tarfile.open(tarball) _extractall(tar) tar.close() # going in the directory subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0]) os.chdir(subdir) log.warn('Now working in %s', subdir) # installing log.warn('Installing Distribute') if not _python_cmd('setup.py', 'install'): log.warn('Something went wrong during the installation.') log.warn('See the error message above.') finally: os.chdir(old_wd) def _build_egg(egg, tarball, to_dir): # extracting the tarball tmpdir = tempfile.mkdtemp() log.warn('Extracting in %s', tmpdir) old_wd = os.getcwd() try: os.chdir(tmpdir) tar = tarfile.open(tarball) _extractall(tar) tar.close() # going in the directory subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0]) os.chdir(subdir) log.warn('Now working in %s', subdir) # building an egg log.warn('Building a Distribute egg in %s', to_dir) _python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir) finally: os.chdir(old_wd) # returning the result log.warn(egg) if not os.path.exists(egg): raise IOError('Could not build the egg.') def _do_download(version, download_base, to_dir, download_delay): egg = os.path.join(to_dir, 'distribute-%s-py%d.%d.egg' % (version, sys.version_info[0], sys.version_info[1])) if not os.path.exists(egg): tarball = download_setuptools(version, download_base, to_dir, download_delay) _build_egg(egg, tarball, to_dir) sys.path.insert(0, egg) import setuptools setuptools.bootstrap_install_from = egg def use_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, download_delay=15, no_fake=True): # making sure we use the absolute path to_dir = os.path.abspath(to_dir) was_imported = 'pkg_resources' in sys.modules or \ 'setuptools' in sys.modules try: try: import pkg_resources if not hasattr(pkg_resources, '_distribute'): if not no_fake: _fake_setuptools() raise ImportError except ImportError: return _do_download(version, download_base, to_dir, download_delay) try: pkg_resources.require("distribute>="+version) return except pkg_resources.VersionConflict: e = sys.exc_info()[1] if was_imported: sys.stderr.write( "The required version of distribute (>=%s) is not available,\n" "and can't be installed while this script is running. Please\n" "install a more recent version first, using\n" "'easy_install -U distribute'." "\n\n(Currently using %r)\n" % (version, e.args[0])) sys.exit(2) else: del pkg_resources, sys.modules['pkg_resources'] # reload ok return _do_download(version, download_base, to_dir, download_delay) except pkg_resources.DistributionNotFound: return _do_download(version, download_base, to_dir, download_delay) finally: if not no_fake: _create_fake_setuptools_pkg_info(to_dir) def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, delay=15): """Download distribute from a specified location and return its filename `version` should be a valid distribute version number that is available as an egg for download under the `download_base` URL (which should end with a '/'). `to_dir` is the directory where the egg will be downloaded. `delay` is the number of seconds to pause before an actual download attempt. """ # making sure we use the absolute path to_dir = os.path.abspath(to_dir) try: from urllib.request import urlopen except ImportError: from urllib2 import urlopen tgz_name = "distribute-%s.tar.gz" % version url = download_base + tgz_name saveto = os.path.join(to_dir, tgz_name) src = dst = None if not os.path.exists(saveto): # Avoid repeated downloads try: log.warn("Downloading %s", url) src = urlopen(url) # Read/write all in one block, so we don't create a corrupt file # if the download is interrupted. data = src.read() dst = open(saveto, "wb") dst.write(data) finally: if src: src.close() if dst: dst.close() return os.path.realpath(saveto) def _patch_file(path, content): """Will backup the file then patch it""" existing_content = open(path).read() if existing_content == content: # already patched log.warn('Already patched.') return False log.warn('Patching...') _rename_path(path) f = open(path, 'w') try: f.write(content) finally: f.close() return True def _same_content(path, content): return open(path).read() == content def _no_sandbox(function): def __no_sandbox(*args, **kw): try: from setuptools.sandbox import DirectorySandbox def violation(*args): pass DirectorySandbox._old = DirectorySandbox._violation DirectorySandbox._violation = violation patched = True except ImportError: patched = False try: return function(*args, **kw) finally: if patched: DirectorySandbox._violation = DirectorySandbox._old del DirectorySandbox._old return __no_sandbox @_no_sandbox def _rename_path(path): new_name = path + '.OLD.%s' % time.time() log.warn('Renaming %s into %s', path, new_name) os.rename(path, new_name) return new_name def _remove_flat_installation(placeholder): if not os.path.isdir(placeholder): log.warn('Unkown installation at %s', placeholder) return False found = False for file in os.listdir(placeholder): if fnmatch.fnmatch(file, 'setuptools*.egg-info'): found = True break if not found: log.warn('Could not locate setuptools*.egg-info') return log.warn('Removing elements out of the way...') pkg_info = os.path.join(placeholder, file) if os.path.isdir(pkg_info): patched = _patch_egg_dir(pkg_info) else: patched = _patch_file(pkg_info, SETUPTOOLS_PKG_INFO) if not patched: log.warn('%s already patched.', pkg_info) return False # now let's move the files out of the way for element in ('setuptools', 'pkg_resources.py', 'site.py'): element = os.path.join(placeholder, element) if os.path.exists(element): _rename_path(element) else: log.warn('Could not find the %s element of the ' 'Setuptools distribution', element) return True def _after_install(dist): log.warn('After install bootstrap.') placeholder = dist.get_command_obj('install').install_purelib _create_fake_setuptools_pkg_info(placeholder) @_no_sandbox def _create_fake_setuptools_pkg_info(placeholder): if not placeholder or not os.path.exists(placeholder): log.warn('Could not find the install location') return pyver = '%s.%s' % (sys.version_info[0], sys.version_info[1]) setuptools_file = 'setuptools-%s-py%s.egg-info' % \ (SETUPTOOLS_FAKED_VERSION, pyver) pkg_info = os.path.join(placeholder, setuptools_file) if os.path.exists(pkg_info): log.warn('%s already exists', pkg_info) return log.warn('Creating %s', pkg_info) f = open(pkg_info, 'w') try: f.write(SETUPTOOLS_PKG_INFO) finally: f.close() pth_file = os.path.join(placeholder, 'setuptools.pth') log.warn('Creating %s', pth_file) f = open(pth_file, 'w') try: f.write(os.path.join(os.curdir, setuptools_file)) finally: f.close() def _patch_egg_dir(path): # let's check if it's already patched pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO') if os.path.exists(pkg_info): if _same_content(pkg_info, SETUPTOOLS_PKG_INFO): log.warn('%s already patched.', pkg_info) return False _rename_path(path) os.mkdir(path) os.mkdir(os.path.join(path, 'EGG-INFO')) pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO') f = open(pkg_info, 'w') try: f.write(SETUPTOOLS_PKG_INFO) finally: f.close() return True def _before_install(): log.warn('Before install bootstrap.') _fake_setuptools() def _under_prefix(location): if 'install' not in sys.argv: return True args = sys.argv[sys.argv.index('install')+1:] for index, arg in enumerate(args): for option in ('--root', '--prefix'): if arg.startswith('%s=' % option): top_dir = arg.split('root=')[-1] return location.startswith(top_dir) elif arg == option: if len(args) > index: top_dir = args[index+1] return location.startswith(top_dir) elif option == '--user' and USER_SITE is not None: return location.startswith(USER_SITE) return True def _fake_setuptools(): log.warn('Scanning installed packages') try: import pkg_resources except ImportError: # we're cool log.warn('Setuptools or Distribute does not seem to be installed.') return ws = pkg_resources.working_set try: setuptools_dist = ws.find(pkg_resources.Requirement.parse('setuptools', replacement=False)) except TypeError: # old distribute API setuptools_dist = ws.find(pkg_resources.Requirement.parse('setuptools')) if setuptools_dist is None: log.warn('No setuptools distribution found') return # detecting if it was already faked setuptools_location = setuptools_dist.location log.warn('Setuptools installation detected at %s', setuptools_location) # if --root or --preix was provided, and if # setuptools is not located in them, we don't patch it if not _under_prefix(setuptools_location): log.warn('Not patching, --root or --prefix is installing Distribute' ' in another location') return # let's see if its an egg if not setuptools_location.endswith('.egg'): log.warn('Non-egg installation') res = _remove_flat_installation(setuptools_location) if not res: return else: log.warn('Egg installation') pkg_info = os.path.join(setuptools_location, 'EGG-INFO', 'PKG-INFO') if (os.path.exists(pkg_info) and _same_content(pkg_info, SETUPTOOLS_PKG_INFO)): log.warn('Already patched.') return log.warn('Patching...') # let's create a fake egg replacing setuptools one res = _patch_egg_dir(setuptools_location) if not res: return log.warn('Patched done.') _relaunch() def _relaunch(): log.warn('Relaunching...') # we have to relaunch the process args = [sys.executable] + sys.argv sys.exit(subprocess.call(args)) def _extractall(self, path=".", members=None): """Extract all members from the archive to the current working directory and set owner, modification time and permissions on directories afterwards. `path' specifies a different directory to extract to. `members' is optional and must be a subset of the list returned by getmembers(). """ import copy import operator from tarfile import ExtractError directories = [] if members is None: members = self for tarinfo in members: if tarinfo.isdir(): # Extract directories with a safe mode. directories.append(tarinfo) tarinfo = copy.copy(tarinfo) tarinfo.mode = 448 # decimal for oct 0700 self.extract(tarinfo, path) # Reverse sort directories. if sys.version_info < (2, 4): def sorter(dir1, dir2): return cmp(dir1.name, dir2.name) directories.sort(sorter) directories.reverse() else: directories.sort(key=operator.attrgetter('name'), reverse=True) # Set correct owner, mtime and filemode on directories. for tarinfo in directories: dirpath = os.path.join(path, tarinfo.name) try: self.chown(tarinfo, dirpath) self.utime(tarinfo, dirpath) self.chmod(tarinfo, dirpath) except ExtractError: e = sys.exc_info()[1] if self.errorlevel > 1: raise else: self._dbg(1, "tarfile: %s" % e) def main(argv, version=DEFAULT_VERSION): """Install or upgrade setuptools and EasyInstall""" tarball = download_setuptools() _install(tarball) if __name__ == '__main__': main(sys.argv[1:]) python-messaging-0.5.12/Makefile0000664000175000017500000000205011707534031016301 0ustar achiangachiangSHELL = /bin/bash VERSION := $(shell python -c 'from messaging import VERSION; print "%s.%s.%s" % VERSION') SOURCES := $(shell rpmbuild --eval '%{_topdir}' 2>/dev/null)/SOURCES PMV := python-messaging-$(VERSION) all: @echo Usage: make deb \[TARGET=ubuntu-lucid\] \| rpm test: nosetests -v -w . messaging/test rpm: @if [ ! -d $(SOURCES) ] ;\ then\ echo 'SOURCES does not exist, are you running on a non RPM based system?';\ exit 1;\ fi tar -zcvf $(SOURCES)/$(PMV).tar.gz --exclude=.git --transform="s/^\./$(PMV)/" . rpmbuild -ba python-messaging.spec deb: @if [ ! -d /var/lib/dpkg ] ;\ then\ echo 'Debian package directory does not exist, are you running on a non Debian based system?';\ exit 1;\ fi @if [ -d packaging/debian/$(TARGET)/debian ] ;\ then\ PKGSOURCE=$(TARGET);\ else\ PKGSOURCE=generic;\ fi;\ tar -C packaging/debian/$$PKGSOURCE -cf - debian | tar -xf - @if ! head -1 debian/changelog | grep -q $(VERSION) ;\ then\ echo Changelog and package version are different;\ exit 1;\ fi dpkg-buildpackage -rfakeroot python-messaging-0.5.12/setup.py0000664000175000017500000000206112041140736016352 0ustar achiangachiangfrom distribute_setup import use_setuptools use_setuptools() from setuptools import setup, find_packages import sys extra = {} if sys.version_info >= (3,): extra['use_2to3'] = True setup(name="python-messaging", version='0.5.10', description='SMS/MMS encoder/decoder', license=open('COPYING').read(), packages=find_packages(), install_requires=['nose'], zip_safe=True, test_suite='nose.collector', classifiers=[ 'Development Status :: 4 - Beta', 'License :: OSI Approved :: GNU General Public License (GPL)', 'Natural Language :: English', 'Operating System :: POSIX :: Linux', 'Programming Language :: Python', 'Programming Language :: Python :: 2.5', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.1', 'Programming Language :: Python :: 3.2', 'Topic :: Communications :: Telephony', ], **extra ) python-messaging-0.5.12/resources/0000775000175000017500000000000011707534031016656 5ustar achiangachiangpython-messaging-0.5.12/resources/pydump.py0000664000175000017500000001231511707534031020550 0ustar achiangachiang# Use this file to dump a python string into a format that wireshark's # text2pcap can interpret and convert to a pcap that wireshark can read # # Suggested workflow: # edit 'data' string to contain the interesting data # python pydump.py > wsp.dmp # text2pcap -o hex -u9200,50 wsp.dmp wsp.pcap # wireshark wsp.pcap # export plain text format from wireshark from struct import unpack # generic WAP Push data = '\x01\x06\x0b\x03\xae\x81\xea\xc3\x95\x8d\x01\xa2\xb4\x84\x03\x05j\n Vodafone\x00E\xc6\x0c\x03wap.meincallya.de/\x00\x08\x01\x03Zum kostenlosen Portal "Mein\x00\x83\x00\x03" - einfach auf den folgenden Link klicken oder die Seite direkt aufrufen. Ihr\x00\x83\x00\x03 Team\x00\x01\x01' """ No. Time Source Destination Protocol Info 1 0.000000 1.1.1.1 2.2.2.2 WSP WSP Push (0x06) (WBXML 1.3, Public ID: "-//WAPFORUM//DTD SI 1.0//EN (Service Indication 1.0)") Frame 1: 218 bytes on wire (1744 bits), 218 bytes captured (1744 bits) Ethernet II, Src: Private_01:01:01 (01:01:01:01:01:01), Dst: MS-NLB-PhysServer-02_02:02:02:02 (02:02:02:02:02:02) Internet Protocol, Src: 1.1.1.1 (1.1.1.1), Dst: 2.2.2.2 (2.2.2.2) User Datagram Protocol, Src Port: wap-wsp (9200), Dst Port: re-mail-ck (50) Wireless Session Protocol, Method: Push (0x06), Content-Type: application/vnd.wap.sic Transaction ID: 0x01 PDU Type: Push (0x06) Headers Length: 11 Content-Type: application/vnd.wap.sic; charset=utf-8 Charset: utf-8 Headers Encoding-Version: 1.5 Content-Length: 162 Push-Flag: (Last push message) .... ...0 = Initiator URI is authenticated: False (0) .... ..0. = Content is trusted: False (0) .... .1.. = Last push message: True (1) WAP Binary XML, Version: 1.3, Public ID: "-//WAPFORUM//DTD SI 1.0//EN (Service Indication 1.0)" Version: 1.3 (0x03) Public Identifier (known): -//WAPFORUM//DTD SI 1.0//EN (Service Indication 1.0) (0x00000005) Character Set: utf-8 (0x0000006a) String table: 10 bytes Start | Length | String 0 | 10 | ' Vodafone' Data representation Level | State | Codepage | WBXML Token Description | Rendering 0 | Tag | T 0 | Known Tag 0x05 (.C) | 1 | Tag | T 0 | Known Tag 0x06 (AC) | 1 | Tag | T 0 | STR_I (Inline string) | 'Zum kostenlosen Portal "Mein' 1 | Tag | T 0 | STR_T (Tableref string) | ' Vodafone' 1 | Tag | T 0 | STR_I (Inline string) | '" - einfach auf den folgenden Link klicken oder die Seite direkt aufrufen. Ihr' 1 | Tag | T 0 | STR_T (Tableref string) | ' Vodafone' 1 | Tag | T 0 | STR_I (Inline string) | ' Team' 1 | Tag | T 0 | END (Known Tag 0x06) | 0 | Tag | T 0 | END (Known Tag 0x05) | """ # MMS WAP Push data = '\x01\x06"application/vnd.wap.mms-message\x00\xaf\x84\x8c\x82\x98NOK5A1ZdFTMYSG4O3VQAAsJv94GoNAAAAAAAA\x00\x8d\x90\x89\x19\x80+447717275049/TYPE=PLMN\x00\x8a\x80\x8e\x02t\x00\x88\x05\x81\x03\x03\xf4\x7f\x83http://promms/servlets/NOK5A1ZdFTMYSG4O3VQAAsJv94GoNAAAAAAAA\x00' """ No. Time Source Destination Protocol Info 1 0.000000 1.1.1.1 2.2.2.2 MMSE MMS m-notification-ind Frame 1: 224 bytes on wire (1792 bits), 224 bytes captured (1792 bits) Ethernet II, Src: Private_01:01:01 (01:01:01:01:01:01), Dst: MS-NLB-PhysServer-02_02:02:02:02 (02:02:02:02:02:02) Internet Protocol, Src: 1.1.1.1 (1.1.1.1), Dst: 2.2.2.2 (2.2.2.2) User Datagram Protocol, Src Port: wap-wsp (9200), Dst Port: re-mail-ck (50) Wireless Session Protocol, Method: Push (0x06), Content-Type: application/vnd.wap.mms-message Transaction ID: 0x01 PDU Type: Push (0x06) Headers Length: 34 Content-Type: application/vnd.wap.mms-message Headers X-Wap-Application-Id: x-wap-application:mms.ua MMS Message Encapsulation, Type: m-notification-ind X-Mms-Message-Type: m-notification-ind (0x82) X-Mms-Transaction-ID: NOK5A1ZdFTMYSG4O3VQAAsJv94GoNAAAAAAAA X-Mms-MMS-Version: 1.0 From: +447717275049/TYPE=PLMN X-Mms-Message-Class: Personal (0x80) X-Mms-Message-Size: 29696 X-Mms-Expiry: 259199.000000000 seconds X-Mms-Content-Location: http://promms/servlets/NOK5A1ZdFTMYSG4O3VQAAsJv94GoNAAAAAAAA """ offset = 0 length = len(data) perline = 8 while True: if offset >= length: break end = offset + perline if end > length: end = length line = data[offset:end] s = '' for c in line: s += " %02x" % unpack('B', c) # 000000 00 e0 1e a7 05 6f 00 10 print "%06x%s" % (offset, s) offset += perline python-messaging-0.5.12/python-messaging.spec0000664000175000017500000000260111707534031021013 0ustar achiangachiang%{!?python_sitelib: %define python_sitelib %(%{__python} -c "from distutils.sysconfig import get_python_lib; print get_python_lib()")} Name: python-messaging Version: %(%{__python} -c 'from messaging import VERSION; print "%s.%s.%s" % VERSION') Release: 1%{?dist} Summary: SMS encoder/decoder library License: GPL Group: Development Source: %{name}-%{version}.tar.gz BuildRoot: %{_tmppath}/%{name}-buildroot BuildArch: noarch BuildRequires: python-devel %if 0%{?fedora} >= 8 BuildRequires: python-setuptools-devel %else BuildRequires: python-setuptools %endif %description Pure python SMS encoder/decoder library %prep %setup -q -n %{name}-%{version} %build %{__python} -c 'import setuptools; execfile("setup.py")' build %install [ "%{buildroot}" != "/" ] && rm -rf %{buildroot} %{__python} -c 'import setuptools; execfile("setup.py")' install -O1 --skip-build --root %{buildroot} --prefix=%{_prefix} %clean [ "%{buildroot}" != "/" ] && rm -rf %{buildroot} %files %{python_sitelib} %defattr(-,root,root,-) %doc README %changelog * Tue Jan 24 2012 - Andrew Bird - 0.5.12 - New release * Tue Aug 30 2011 - Andrew Bird - 0.5.11 - New release * Mon Jun 06 2011 - Andrew Bird - 0.5.10 - Initial release - Spec file tested on Fedora 14 / 15 and OpenSUSE 11.4 python-messaging-0.5.12/CHANGELOG0000664000175000017500000000363511707534031016065 0ustar achiangachiang++++++++++++++++++++++++++++++++++++++++++++++ python-messaging-0.5.9 Overview of changes since python-messaging-0.5 ++++++++++++++++++++++++++++++++++++++++++++++ This is an interim release of python-messaging. List of changes: * See Git log for details ++++++++++++++++++++++++++++++++++++++++++++++ python-messaging-0.5 Overview of changes since python-messaging-0.4 ++++++++++++++++++++++++++++++++++++++++++++++ This is a new major stable release of python-messaging. List of changes: * Py3K fixes * GSM 7bit encoding fixes * Additional testing ++++++++++++++++++++++++++++++++++++++++++++++ python-messaging-0.4 Overview of changes since python-messaging-0.3 ++++++++++++++++++++++++++++++++++++++++++++++ This is a new major stable release of python-messaging. List of changes: * Py3K support * GSM 7bit decoding fixes ++++++++++++++++++++++++++++++++++++++++++++++ python-messaging-0.3 Overview of changes since python-messaging-0.2 ++++++++++++++++++++++++++++++++++++++++++++++ This is a new major stable release of python-messaging. List of changes: * API has changed, decode_pdu returns a dictionary now * SMS status report encoding * SMS status report decoding by the MobileManager project * Decoder completely revamped (code way cleaner) * Support for alphanumeric addresses contributed by Andrew Bird * Time offset fix by Andrew Bird * Debian package migrated to pycentral * Switched to distribute ++++++++++++++++++++++++++++++++++++++++++++++ python-messaging-0.2 Overview of changes since python-messaging-0.1 ++++++++++++++++++++++++++++++++++++++++++++++ This is a new major stable release of python-messaging. List of changes: * Fix the case where there is no SMSC * Some multi-part fixes pulled from mobile-manager * Test coverage increased * Docstrings updated to rst * Some packaging fixes * Some small code simplifications python-messaging-0.1 ++++++++++++++++++++ * Initial release python-messaging-0.5.12/COPYING0000664000175000017500000004310311707534031015700 0ustar achiangachiang GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License.