django-mailer-0.2a1.dev3/0000755000175000017500000000000011620203613014114 5ustar chuckchuckdjango-mailer-0.2a1.dev3/docs/0000775000175000017500000000000011620203613015046 5ustar chuckchuckdjango-mailer-0.2a1.dev3/docs/usage.txt0000664000175000017500000000577211620203613016726 0ustar chuckchuck ===== Usage ===== django-mailer is asynchronous so in addition to putting mail on the queue you need to periodically tell it to clear the queue and actually send the mail. The latter is done via a command extension. Putting Mail On The Queue ========================= Because django-mailer currently uses the same function signature as Django's core mail support you can do the following in your code:: # favour django-mailer but fall back to django.core.mail from django.conf import settings if "mailer" in settings.INSTALLED_APPS: from mailer import send_mail else: from django.core.mail import send_mail and then just call send_mail like you normally would in Django:: send_mail(subject, message_body, settings.DEFAULT_FROM_EMAIL, recipients) or to send a HTML e-mail (this function is **not** in Django):: send_html_mail(subject, message_plaintext, message_html, settings.DEFAULT_FROM_EMAIL, recipients) Additionally you can send all the admins as specified in the ``ADMIN`` setting by calling:: mail_admins(subject, message_body) or all managers as defined in the ``MANAGERS`` setting by calling:: mail_managers(subject, message_body) Clear Queue With Command Extensions =================================== With mailer in your INSTALLED_APPS, there will be two new manage.py commands you can run: * ``send_mail`` will clear the current message queue. If there are any failures, they will be marked deferred and will not be attempted again by ``send_mail``. * ``retry_deferred`` will move any deferred mail back into the normal queue (so it will be attempted again on the next ``send_mail``). You may want to set these up via cron to run regularly:: * * * * * (cd $PINAX; /usr/local/bin/python2.5 manage.py send_mail >> $PINAX/cron_mail.log 2>&1) 0,20,40 * * * * (cd $PINAX; /usr/local/bin/python2.5 manage.py retry_deferred >> $PINAX/cron_mail_deferred.log 2>&1) This attempts to send mail every minute with a retry on failure every 20 minutes. ``manage.py send_mail`` uses a lock file in case clearing the queue takes longer than the interval between calling ``manage.py send_mail``. Note that if your project lives inside a virtualenv, you also have to execute this command from the virtualenv. The same, naturally, applies also if you're executing it with cron. The `Pinax documentation`_ explains that in more details. .. _pinax documentation: http://pinaxproject.com/docs/dev/deployment.html#sending-mail-and-notices Using EMAIL_BACKEND =================== To automatically switch all your mail to use django-mailer, instead of changing imports you can also use the EMAIL_BACKEND feature that was introduced in Django 1.2. In your settings file, you first have to set EMAIL_BACKEND:: EMAIL_BACKEND = "mailer.backend.DbBackend" If you were previously using a non-default EMAIL_BACKEND, you need to configure the MAILER_EMAIL_BACKEND setting, so that django-mailer knows how to actually send the mail:: MAILER_EMAIL_BACKEND = "your.actual.EmailBackend" django-mailer-0.2a1.dev3/docs/index.txt0000664000175000017500000000011711620203613016715 0ustar chuckchuck ============= django-mailer ============= Contents: .. toctree:: usage django-mailer-0.2a1.dev3/AUTHORS0000664000175000017500000000026211620203613015166 0ustar chuckchuck The PRIMARY AUTHORS are: * James Tauber * Brian Rosner ADDITIONAL CONTRIBUTORS include: * Michael Trier * Doug Napoleone * Jannis Leidel * Luke Plant django-mailer-0.2a1.dev3/setup.py0000664000175000017500000000147111620203613015633 0ustar chuckchuckfrom distutils.core import setup setup( name="django-mailer", version=__import__("mailer").__version__, description="A reusable Django app for queuing the sending of email", long_description=open("docs/usage.txt").read(), author="James Tauber", author_email="jtauber@jtauber.com", url="http://code.google.com/p/django-mailer/", packages=[ "mailer", "mailer.management", "mailer.management.commands", ], package_dir={"mailer": "mailer"}, classifiers=[ "Development Status :: 4 - Beta", "Environment :: Web Environment", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Framework :: Django", ] ) django-mailer-0.2a1.dev3/.git/0000775000175000017500000000000011620203613014757 5ustar chuckchuckdjango-mailer-0.2a1.dev3/.git/hooks/0000775000175000017500000000000011620203612016101 5ustar chuckchuckdjango-mailer-0.2a1.dev3/.git/hooks/pre-rebase.sample0000775000175000017500000001155311620203612021341 0ustar chuckchuck#!/bin/sh # # Copyright (c) 2006, 2008 Junio C Hamano # # The "pre-rebase" hook is run just before "git rebase" starts doing # its job, and can prevent the command from running by exiting with # non-zero status. # # The hook is called with the following parameters: # # $1 -- the upstream the series was forked from. # $2 -- the branch being rebased (or empty when rebasing the current branch). # # This sample shows how to prevent topic branches that are already # merged to 'next' branch from getting rebased, because allowing it # would result in rebasing already published history. publish=next basebranch="$1" if test "$#" = 2 then topic="refs/heads/$2" else topic=`git symbolic-ref HEAD` || exit 0 ;# we do not interrupt rebasing detached HEAD fi case "$topic" in refs/heads/??/*) ;; *) exit 0 ;# we do not interrupt others. ;; esac # Now we are dealing with a topic branch being rebased # on top of master. Is it OK to rebase it? # Does the topic really exist? git show-ref -q "$topic" || { echo >&2 "No such branch $topic" exit 1 } # Is topic fully merged to master? not_in_master=`git rev-list --pretty=oneline ^master "$topic"` if test -z "$not_in_master" then echo >&2 "$topic is fully merged to master; better remove it." exit 1 ;# we could allow it, but there is no point. fi # Is topic ever merged to next? If so you should not be rebasing it. only_next_1=`git rev-list ^master "^$topic" ${publish} | sort` only_next_2=`git rev-list ^master ${publish} | sort` if test "$only_next_1" = "$only_next_2" then not_in_topic=`git rev-list "^$topic" master` if test -z "$not_in_topic" then echo >&2 "$topic is already up-to-date with master" exit 1 ;# we could allow it, but there is no point. else exit 0 fi else not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"` /usr/bin/perl -e ' my $topic = $ARGV[0]; my $msg = "* $topic has commits already merged to public branch:\n"; my (%not_in_next) = map { /^([0-9a-f]+) /; ($1 => 1); } split(/\n/, $ARGV[1]); for my $elem (map { /^([0-9a-f]+) (.*)$/; [$1 => $2]; } split(/\n/, $ARGV[2])) { if (!exists $not_in_next{$elem->[0]}) { if ($msg) { print STDERR $msg; undef $msg; } print STDERR " $elem->[1]\n"; } } ' "$topic" "$not_in_next" "$not_in_master" exit 1 fi exit 0 <<\DOC_END ################################################################ This sample hook safeguards topic branches that have been published from being rewound. The workflow assumed here is: * Once a topic branch forks from "master", "master" is never merged into it again (either directly or indirectly). * Once a topic branch is fully cooked and merged into "master", it is deleted. If you need to build on top of it to correct earlier mistakes, a new topic branch is created by forking at the tip of the "master". This is not strictly necessary, but it makes it easier to keep your history simple. * Whenever you need to test or publish your changes to topic branches, merge them into "next" branch. The script, being an example, hardcodes the publish branch name to be "next", but it is trivial to make it configurable via $GIT_DIR/config mechanism. With this workflow, you would want to know: (1) ... if a topic branch has ever been merged to "next". Young topic branches can have stupid mistakes you would rather clean up before publishing, and things that have not been merged into other branches can be easily rebased without affecting other people. But once it is published, you would not want to rewind it. (2) ... if a topic branch has been fully merged to "master". Then you can delete it. More importantly, you should not build on top of it -- other people may already want to change things related to the topic as patches against your "master", so if you need further changes, it is better to fork the topic (perhaps with the same name) afresh from the tip of "master". Let's look at this example: o---o---o---o---o---o---o---o---o---o "next" / / / / / a---a---b A / / / / / / / / c---c---c---c B / / / / \ / / / / b---b C \ / / / / / \ / ---o---o---o---o---o---o---o---o---o---o---o "master" A, B and C are topic branches. * A has one fix since it was merged up to "next". * B has finished. It has been fully merged up to "master" and "next", and is ready to be deleted. * C has not merged to "next" at all. We would want to allow C to be rebased, refuse A, and encourage B to be deleted. To compute (1): git rev-list ^master ^topic next git rev-list ^master next if these match, topic has not merged in next at all. To compute (2): git rev-list master..topic if this is empty, it is fully merged to "master". DOC_END django-mailer-0.2a1.dev3/.git/hooks/post-receive.sample0000775000175000017500000000105011620203612021710 0ustar chuckchuck#!/bin/sh # # An example hook script for the "post-receive" event. # # The "post-receive" script is run after receive-pack has accepted a pack # and the repository has been updated. It is passed arguments in through # stdin in the form # # For example: # aa453216d1b3e49e7f6f98441fa56946ddcd6a20 68f7abf4e6f922807889f52bc043ecd31b79f814 refs/heads/master # # see contrib/hooks/ for a sample, or uncomment the next line and # rename the file to "post-receive". #. /usr/share/doc/git-core/contrib/hooks/post-receive-email django-mailer-0.2a1.dev3/.git/hooks/commit-msg.sample0000775000175000017500000000160011620203612021360 0ustar chuckchuck#!/bin/sh # # An example hook script to check the commit log message. # Called by "git commit" with one argument, the name of the file # that has the commit message. The hook should exit with non-zero # status after issuing an appropriate message if it wants to stop the # commit. The hook is allowed to edit the commit message file. # # To enable this hook, rename this file to "commit-msg". # Uncomment the below to add a Signed-off-by line to the message. # Doing this in a hook is a bad idea in general, but the prepare-commit-msg # hook is more suited to it. # # SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') # grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" # This example catches duplicate Signed-off-by lines. test "" = "$(grep '^Signed-off-by: ' "$1" | sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || { echo >&2 Duplicate Signed-off-by lines. exit 1 } django-mailer-0.2a1.dev3/.git/hooks/post-commit.sample0000775000175000017500000000024011620203612021556 0ustar chuckchuck#!/bin/sh # # An example hook script that is called after a successful # commit is made. # # To enable this hook, rename this file to "post-commit". : Nothing django-mailer-0.2a1.dev3/.git/hooks/update.sample0000775000175000017500000000703311620203612020574 0ustar chuckchuck#!/bin/sh # # An example hook script to blocks unannotated tags from entering. # Called by "git receive-pack" with arguments: refname sha1-old sha1-new # # To enable this hook, rename this file to "update". # # Config # ------ # hooks.allowunannotated # This boolean sets whether unannotated tags will be allowed into the # repository. By default they won't be. # hooks.allowdeletetag # This boolean sets whether deleting tags will be allowed in the # repository. By default they won't be. # hooks.allowmodifytag # This boolean sets whether a tag may be modified after creation. By default # it won't be. # hooks.allowdeletebranch # This boolean sets whether deleting branches will be allowed in the # repository. By default they won't be. # hooks.denycreatebranch # This boolean sets whether remotely creating branches will be denied # in the repository. By default this is allowed. # # --- Command line refname="$1" oldrev="$2" newrev="$3" # --- Safety check if [ -z "$GIT_DIR" ]; then echo "Don't run this script from the command line." >&2 echo " (if you want, you could supply GIT_DIR then run" >&2 echo " $0 )" >&2 exit 1 fi if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then echo "Usage: $0 " >&2 exit 1 fi # --- Config allowunannotated=$(git config --bool hooks.allowunannotated) allowdeletebranch=$(git config --bool hooks.allowdeletebranch) denycreatebranch=$(git config --bool hooks.denycreatebranch) allowdeletetag=$(git config --bool hooks.allowdeletetag) allowmodifytag=$(git config --bool hooks.allowmodifytag) # check for no description projectdesc=$(sed -e '1q' "$GIT_DIR/description") case "$projectdesc" in "Unnamed repository"* | "") echo "*** Project description file hasn't been set" >&2 exit 1 ;; esac # --- Check types # if $newrev is 0000...0000, it's a commit to delete a ref. zero="0000000000000000000000000000000000000000" if [ "$newrev" = "$zero" ]; then newrev_type=delete else newrev_type=$(git cat-file -t $newrev) fi case "$refname","$newrev_type" in refs/tags/*,commit) # un-annotated tag short_refname=${refname##refs/tags/} if [ "$allowunannotated" != "true" ]; then echo "*** The un-annotated tag, $short_refname, is not allowed in this repository" >&2 echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2 exit 1 fi ;; refs/tags/*,delete) # delete tag if [ "$allowdeletetag" != "true" ]; then echo "*** Deleting a tag is not allowed in this repository" >&2 exit 1 fi ;; refs/tags/*,tag) # annotated tag if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1 then echo "*** Tag '$refname' already exists." >&2 echo "*** Modifying a tag is not allowed in this repository." >&2 exit 1 fi ;; refs/heads/*,commit) # branch if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then echo "*** Creating a branch is not allowed in this repository" >&2 exit 1 fi ;; refs/heads/*,delete) # delete branch if [ "$allowdeletebranch" != "true" ]; then echo "*** Deleting a branch is not allowed in this repository" >&2 exit 1 fi ;; refs/remotes/*,commit) # tracking branch ;; refs/remotes/*,delete) # delete tracking branch if [ "$allowdeletebranch" != "true" ]; then echo "*** Deleting a tracking branch is not allowed in this repository" >&2 exit 1 fi ;; *) # Anything else (is there anything else?) echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2 exit 1 ;; esac # --- Finished exit 0 django-mailer-0.2a1.dev3/.git/hooks/pre-commit.sample0000775000175000017500000000305211620203612021363 0ustar chuckchuck#!/bin/sh # # An example hook script to verify what is about to be committed. # Called by "git commit" with no arguments. The hook should # exit with non-zero status after issuing an appropriate message if # it wants to stop the commit. # # To enable this hook, rename this file to "pre-commit". if git rev-parse --verify HEAD >/dev/null 2>&1 then against=HEAD else # Initial commit: diff against an empty tree object against=4b825dc642cb6eb9a060e54bf8d69288fbee4904 fi # If you want to allow non-ascii filenames set this variable to true. allownonascii=$(git config hooks.allownonascii) # Cross platform projects tend to avoid non-ascii filenames; prevent # them from being added to the repository. We exploit the fact that the # printable range starts at the space character and ends with tilde. if [ "$allownonascii" != "true" ] && # Note that the use of brackets around a tr range is ok here, (it's # even required, for portability to Solaris 10's /usr/bin/tr), since # the square bracket bytes happen to fall in the designated range. test "$(git diff --cached --name-only --diff-filter=A -z $against | LC_ALL=C tr -d '[ -~]\0')" then echo "Error: Attempt to add a non-ascii file name." echo echo "This can cause problems if you want to work" echo "with people on other platforms." echo echo "To be portable it is advisable to rename the file ..." echo echo "If you know what you are doing you can disable this" echo "check using:" echo echo " git config hooks.allownonascii true" echo exit 1 fi exec git diff-index --check --cached $against -- django-mailer-0.2a1.dev3/.git/hooks/prepare-commit-msg.sample0000775000175000017500000000232711620203612023023 0ustar chuckchuck#!/bin/sh # # An example hook script to prepare the commit log message. # Called by "git commit" with the name of the file that has the # commit message, followed by the description of the commit # message's source. The hook's purpose is to edit the commit # message file. If the hook fails with a non-zero status, # the commit is aborted. # # To enable this hook, rename this file to "prepare-commit-msg". # This hook includes three examples. The first comments out the # "Conflicts:" part of a merge commit. # # The second includes the output of "git diff --name-status -r" # into the message, just before the "git status" output. It is # commented because it doesn't cope with --amend or with squashed # commits. # # The third example adds a Signed-off-by line to the message, that can # still be edited. This is rarely a good idea. case "$2,$3" in merge,) /usr/bin/perl -i.bak -ne 's/^/# /, s/^# #/#/ if /^Conflicts/ .. /#/; print' "$1" ;; # ,|template,) # /usr/bin/perl -i.bak -pe ' # print "\n" . `git diff --cached --name-status -r` # if /^#/ && $first++ == 0' "$1" ;; *) ;; esac # SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') # grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" django-mailer-0.2a1.dev3/.git/hooks/pre-applypatch.sample0000775000175000017500000000061611620203612022243 0ustar chuckchuck#!/bin/sh # # An example hook script to verify what is about to be committed # by applypatch from an e-mail message. # # The hook should exit with non-zero status after issuing an # appropriate message if it wants to stop the commit. # # To enable this hook, rename this file to "pre-applypatch". . git-sh-setup test -x "$GIT_DIR/hooks/pre-commit" && exec "$GIT_DIR/hooks/pre-commit" ${1+"$@"} : django-mailer-0.2a1.dev3/.git/hooks/applypatch-msg.sample0000775000175000017500000000070411620203612022241 0ustar chuckchuck#!/bin/sh # # An example hook script to check the commit log message taken by # applypatch from an e-mail message. # # The hook should exit with non-zero status after issuing an # appropriate message if it wants to stop the commit. The hook is # allowed to edit the commit message file. # # To enable this hook, rename this file to "applypatch-msg". . git-sh-setup test -x "$GIT_DIR/hooks/commit-msg" && exec "$GIT_DIR/hooks/commit-msg" ${1+"$@"} : django-mailer-0.2a1.dev3/.git/hooks/post-update.sample0000775000175000017500000000027511620203612021560 0ustar chuckchuck#!/bin/sh # # An example hook script to prepare a packed repository for use over # dumb transports. # # To enable this hook, rename this file to "post-update". exec git update-server-info django-mailer-0.2a1.dev3/.git/logs/0000775000175000017500000000000011620203613015723 5ustar chuckchuckdjango-mailer-0.2a1.dev3/.git/logs/HEAD0000664000175000017500000000030211620203613016342 0ustar chuckchuck0000000000000000000000000000000000000000 840d25bb9db9fbc801b9226607ddce49f4ac37c5 Chuck Short 1312884619 -0400 clone: from git://github.com/jtauber/django-mailer.git django-mailer-0.2a1.dev3/.git/logs/refs/0000775000175000017500000000000011620203613016662 5ustar chuckchuckdjango-mailer-0.2a1.dev3/.git/logs/refs/heads/0000775000175000017500000000000011620203613017746 5ustar chuckchuckdjango-mailer-0.2a1.dev3/.git/logs/refs/heads/master0000664000175000017500000000030211620203613021157 0ustar chuckchuck0000000000000000000000000000000000000000 840d25bb9db9fbc801b9226607ddce49f4ac37c5 Chuck Short 1312884619 -0400 clone: from git://github.com/jtauber/django-mailer.git django-mailer-0.2a1.dev3/.git/info/0000775000175000017500000000000011620203612015711 5ustar chuckchuckdjango-mailer-0.2a1.dev3/.git/info/exclude0000664000175000017500000000036011620203612017264 0ustar chuckchuck# git ls-files --others --exclude-from=.git/info/exclude # Lines that start with '#' are comments. # For a project mostly in C, the following would be a good set of # exclude patterns (uncomment them if you want to use them): # *.[oa] # *~ django-mailer-0.2a1.dev3/.git/packed-refs0000664000175000017500000000013611620203612017065 0ustar chuckchuck# pack-refs with: peeled 840d25bb9db9fbc801b9226607ddce49f4ac37c5 refs/remotes/origin/master django-mailer-0.2a1.dev3/.git/config0000664000175000017500000000041311620203613016145 0ustar chuckchuck[core] repositoryformatversion = 0 filemode = true bare = false logallrefupdates = true [remote "origin"] fetch = +refs/heads/*:refs/remotes/origin/* url = git://github.com/jtauber/django-mailer.git [branch "master"] remote = origin merge = refs/heads/master django-mailer-0.2a1.dev3/.git/index0000664000175000017500000000270011620203613016010 0ustar chuckchuckDIRCNANAv=)lDum0+AUTHORSNANA0Tqp&G3mj;pqLICENSENANA9mH.r뜄y%`ycz MANIFEST.inNANAri &tTӡ۫READMENANACOL OZGGwCN\kdocs/index.txtNANAD Ss-^ڤmailer/engine.pyNANAJ;*;iAҒMQmailer/lockfile.pyNANAL⛲CK)wZSmailer/management/__init__.pyNANAN⛲CK)wZS&mailer/management/commands/__init__.pyNANAO(_F3.$tlU},mailer/management/commands/retry_deferred.pyNANAP#ed7O-yL9'mailer/management/commands/send_mail.pyNANAQ'z1!7+#mailer/models.pyNANA9pf뀬۳vk8\K$setup.pyC~/3zS7ux django-mailer-0.2a1.dev3/.git/objects/0000775000175000017500000000000011620203612016407 5ustar chuckchuckdjango-mailer-0.2a1.dev3/.git/objects/info/0000775000175000017500000000000011620203612017342 5ustar chuckchuckdjango-mailer-0.2a1.dev3/.git/objects/pack/0000775000175000017500000000000011620203613017326 5ustar chuckchuckdjango-mailer-0.2a1.dev3/.git/objects/pack/pack-6c16e30bfb1f050d0c140eaac00db0a4166a2ac8.pack0000444000175000017500000016776111620203612026711 0ustar chuckchuckPACKx[ 0sem^ "۔z~Gg!ɠA.&RPT(K6nyd9E+nr~lb.̣t nmgi?>_CF& '{U/ 8h?uERx;n0D{+' p&7Xr`S4(# ͛EL2jK$ @@rL.P xsEDX3Ϲ$+"oOnQe"E!"Mչ/ﶭ)yrj6:uCN_/6DNkh?ozׇl]Kxn E k3@ lWƘ5a/Awֵ}tjavh&3~lɡM0J^3vW,*O"(izG;Tnb EÀz jV\DLp{܇yø(o ][9i8 #D-vRp%Ly K\[sigXjo|Z 5}; !UFGC 2M@D> #SGrx 0 3ʣq] !l` Tӝ՜b" x RH.rDw{R5/ J # ^HR,)23: = : g?>f^]Z 8O=1ɢZ5lu?|ExKn D>E_`@hjcFQ'+SUo"] qi p&)2SΤ䕞uL:LAMh6xi սH~u>ĺAޓ!'$[]z]jkr\s&WHzK:Rrpxl )\xAj0@ѽN1i%{zh&qkz߼2&Z%JذE"fkT9\m!V&z0 %>e26 ,N{az|7C=*a~0 3~{im*IsED g{9((4Rr]縷!7_RA#?#.0;=b01QD}ߣm䨷uגn۴O`5C4g;Z&?2 %}p޿}km^_.!˸mm{KY#iT8;sB{ewm~_x= 1>^X2If^!ɎfAooSǾ.siMwZLXe|.00,奾dCxAN0E>pIlcB $P)bܟ#zғoUd FOfړ`EqzBwz~Jn"2}~@Cɑ,D7:kom.N5qsYT8z X'zg`v?%v |]^ϟ09sZd,J^̜]}P̺W xQj!@=\ ]Q#3:&®u }7,H9$ofA%n"50K>u0qHItb6Gr%h7yoz 6tx FeNOvPt1,IReLaQx[J1E{D;TUO'33.%qr!rJEou$93*bcmܖo&.D rW*%LG^6>}lُ&^+oRMp!`Jxu6C&+]/8X_x;n0D{+'-IFO,`S4(#R`fftf(\(A"9 \b\7=:Tt7k.Q8Ce*,d5 8CBC ں:Wݶ:k>/>UG.c ε VJWu^_}0=]x=j1^E@$v"WF+Y{1@m3( U.9}MXʺi*(6Z}:b9J8BHbIQc\7v~)4AMiD^[OS)\K{- .{9 yR<O.xn0 Dw} ʖ%(tϐ?(hBr`CB?-tdFC8Hb0h oZTJIRJѤ,oge-[n_]$-sY/SSxAj0E: FeK&E05lqBn_%G})GFB?0Z;^8t=zya,[XGP6mCCDC\=cU?ͻ4?RM Mf9@kuV[3nV.Kles$Ox$ /&Ŕ/PXοWx1N0>)H!홐%WS퉀 P'Va@G蓡M1julEtdݩrn4)'6N>B#A8ޠUJ`::gp=W:)=x&>s{m7Veۖ{+rl`_L E`e-ywoiC+X\n;%w߻]xAj0E:KȒ<$hE3&\luB/߃_WU[3c$؊ؓWW-!qfis9X%e~WMP_/ͳ4)ɑ %OExAhdVhlsͽ¦MqYJQR`8=|8CZ+2 82b(v&k~ia@_)H2xQn0D9^ `* iFM(Eb/ 88/7z7!JV\ gXH3XQWJɱ2cJ{"Uc/,M[&8p-l.152EU*T6"YbIS޿?vSl=7_Rh^GZ|u|w3E3[ _ƕx]j!]@/G_(t ݁sA@87/7}ip*v+uX)qMq;LU} kiT?5A3_-z{#,{Jza,յV8d7Ez"c~RTiޞĉ)~HTx 0 E nb6Hb" JF !'H:Q?%%$#gҬSNdUZ7b(#cO6Z75ݟ½n68c e>\2~ ;.wk*%Hsض/6*%jLx 1@T1 dg"b vDd+]"Vt1d2LNg6QG ^eQOߥpxfcz!t:$VɸV5õ_*Nr|Yض3pFT_ϫp !WxKKEŚxQj0 wBذa#EvqWz|.KQ9TG(<ϬDiA} gK`*I=LjՆECbO' slo/\ڎ*RϺs, by^Ɛw^g M*RRWhMx 0Db%%ЁKBJ4FzLQ8QaèG-G&M(>QdbTnޤ!)c(F> erVJE6x V-<5z<,JCǺWPIu"K>s-"qmK.MfxKN1 9EvGB{6Iݍ2qvOW5L !3pB.f4 q۴Y ( z7ZL[m$w4М#5Bl>a_"}o\ϸ\WY>뾞-:FpAeNk^u\&cʽxZvN8VxAn0 z?Ђ,hν%S [ hz[,vvW3;;RN3F?͘FUM9t.L.@#lС&)*|[X6*kj;twh/nEÚ9j"e؏Iw( "+ GZř"lEx>ͥ񓻐35)Kw5RkriڜxOj0+^,PSKO^VqKF ^3%C [N3ZA^N91S, eZe0BuV{Tn)45hKo+k9 /)/2ap 9]GRF XxsP exʼnmUnx翶fOޓܾuc)ж#( ʉm sLK2{kW^( B.pOxj0z՟-PBOV-E-kL{m>oޘ 1dC =Mƅ"FTl2NK>jRh1途<$/jT BpTA*= b˒{疩[}mt](h+^GhH|-w5]>TśxN0DhʖRJ̥@ui7 M&3'3@mOƪJcڶ\Lq=j,.)2FwՍҴhkRDkK< 9`״Dp_qg ҥ*US+]pAes`tM²7! J.S&![~ρaÖ]|DP!ik#[ę9~ezy{0Ў:_ˢ301_8&O\)greq2dnºxY[Vx[N0D+$vX :1Mqy잔%0_ʹZ0=uiȣV\RŌԠѢ؊DiYDžrV :0k[Jךle9g ЉIɦi"Hvu{zNlZ) 7ȥALߐp7'0tݜZ}>,]>lK:̥+v`ܻc|:J>{ >R i_pxMn }E̟1*몫^fpHmN_#t'͓7ZEƣ  n乙F"DaDpbnkmx0s/% P8Ζ]K.tWz{cyB*eGa/ eRk4ĭ,Deׂ3eBi45@~%uՒxKn0DƟQ""%݀3ݓEQs>(G#33wI9Xś-cm9˴i7*619a&Q .hAk.iS ;|aKHs~6pT/}M"u~C' [GDxV{,ȅVKlCH;-* 6b:Y௰=]矲v!b,9/]x/tQ9&ƋhѺ˶ouؤF{^l~1xMn } z =R,Tr4f"Flz8 DS)y+<)8ゖQ YbP>s `)cHxk;G'ޞ>TjnoTH%NI‡HjVz{Awo,we~g-iimY15iΣ;Q.SAf)+˸"Kdxj0~֟eRh-Ҟ]9j)Jk60̔$GBvN gA^wj'J Lqmc(Ң(=iD1:Peo'x)vFxxB~W,-.|\b)?-P23 ĹP9.mC/6RL~v/~Ji!zNu5JrFi\olS[:o֠䦗[ںsZu؊@{xn D|+b1QU\vqhl_O\F#Lݙ1i7bl#C۠'C4; 1G5D5làX^#`+5 ssN7D3g<(=hz=O7P-kOʃ r2sd\a+?p΀aZ6̸~kbIUY&:¹먭/ERC!>]c4Zg2zvF8XKJiΒwz8xAn E}E`(RTR0aurH_+G=鿯%☀ӽh' DXhк`uԜѻ&ݏ`5Aн]*ߨ|1s\R}`, pIo^?[|-L-x k^ZERayzy8 1nEeKk˾x[R㺛j*vۺ][#=l4V>$΃Y[߳ehxOAn0Wm QUC? ]7`#0i:J3YLڈY(6jLiI"b5JjE#ze8Gm%dC.cZ^#Mq< _Oz7JH% CSJ'RZOu9%e&\:mG[ LcҺ+KGvȚ xKn }E6mK!46X6rn7@JCE\{Υ[ &;v ^IIsnNYaV܇zQq,y=v/Wk97+}yRS\H47"m[&!TVɍ8JvP.2ɅGfD})?(#-ivƕ5,uͭz"<ős~k~{+xcGUucp 6"N_ޠ~=!"tȄ8'vA;8W ŬSe: 3,ܣ.Os+^jr,S#oxFmV-˓ԅasi=-nߡd 1*,SR~ip"f.e^ٗ=m=: =YK;-W(F*5/xnkxN0<\B 7܅qaP^.1ql&gs2y%'Т- 1\2Bu)R+ ص-ΡSvТh^N*BaHel[ OOL wo<χ,}=т7p)l\3]#H~=2Sm3$Q.HXdL {Nu!1Dv6(%Eֻlqo>bO*H~'/R`68e _#^h}@0tS<͆ύ[Opq&=SW_y\Z;,a0jEuK#}S[Ǹ,2 ZJ1_pKkg9n)eY KRxVvV jFq̅Ky|jb4s`jx[n DY<|**u<.c,B*uuhtF.NI91g6Y{Qeou t1!{I brvL0G >}reb&4<8ŽVz||k\V`Ɩ{%~工ڰԺk<رv=k%=9bD " drW) +C UD9) c^y $Xd0AxUsE9 Q PY* hR&+\զRRx>]Sk#}c ![ RhOZhi$ Z.6υ4 }O*"=0xϼV=3fPvJGDV a;9s,jq'V{oR&ᣬ+ػ {fv4a'eĎSBnVGG:tz <=x++&x5qPmp}\b^@FÓ:%t[[l+Լ1G<}{ɑkŽGeM֯$W½ui<8f_fwvVyL?y#BpvnL}!pm'xm_{۷UQ>w(=a*ta5Gi58H)r4TiJ8+m?ؒ\+tCxN!Dn.Ӛhb`-Bâ~4dND,OƹYF唴H*8ŌqȮRnZP,NK" N!ξϴ IQ*SXZLgŧ.QZ=k'F]gˎr8ROvV%RZS^~]omؚ߾3OSk0kWL ZʟIpFgq2\() r)Y5~BuxxP[n0_&AU?z!3@ $(lPX.ZREBnJzɁ\LT NrBhEm+AZ4uQ=32OՅi}r::/Duk(s[}!8˱5P&XVB{_S:{3toj b S)roT?SnƔƅ|BzKGoB([kdk&9Lyߝ1a4VivB xMk0 dcRJ,-lXrNb)쿯CwM=4ntab/kUUcǾ!b\u&['WPL+j5&'JԢ朘$}&8:?:o%Ô? 4S|%/u K%u61 !g<-T4oZ%_\a,h^/,2|vpS1J/ @׬o8 j2hcCrxu|;|};?>NO㦜W*y[c\nwK )`]K:_y^ްUޤ*X* ӢE ȏx]n 9žWĀ1?QUE z BI޾NyZ}#M'D&'(dY()QN^3!zU~Y?91ܬA%c!,*|"Léab[@*iWEb;]?R@+aF=?[ µr`,oʗts0KgҡV[g<NGÅr{9bt&_lpxMn0 $QUzJs$(TAzQc/bVhk䬜~l6x;K`"Ry3X7GfU΍Jh9;+k`Bz9i>K4 HXݖ6׋0Qs.%nq_Cf]a/d~3Rռ fg&>/xx[n EY~V$!Ujp:ce֩r 0+%^dr0Ye"71V0BX4I&턳|gŌfG GRlT"fx_9WΥ37T|GBjXP7%{re do,aYC3Oz;qMܭ);.m_/A&#[LrF[ |L/)apxOn >@ T]zN<$Dvbɲe"&tAzji[RҐBg^ <ʈHd hqņhN[k |$?!Ux]Oi>le ^KeCl%_W2/fT'<Ã-rZؘ*=VZcӄ cN*1q&=4˞ FsUυ ]2}`@uxPKN0ChF͚5H^;M8زl/*Pf6 &tAG-4J[6'Jѓ4q˩[Bv+ǕDۯK.W H`]mV~O^((u}Kg xNIn0 XKȹ Z'er_ AZ!5MhgY9E,YdbKT)#n}]):;{8_w\^R?@{xSLj:bЏCͰ~~O. B8M؄*"G4ter#S&~6e_Wc@5[/UR.8OFO0NsxKn0 D:jEu׽>ĖE.W)zrCq87DpD R%;S2.# 7c3xHC6D7Y(#*E/{>۵g1^(2Vz]qOJ.P*=S 8 q YJJK:ie/Z}Yiv3w\,4e3jc0:qxP1n0 jђe*(̝F't_ zx8n8 0' #Fgm\v܏` .sTO0F 5';N@18E\K?po7%o_lj꺈YRmޕ1f|W򍶹G KJ柼vR~6&"z&` eBCV%cSxLKN0Ch^~MF͚5xK;i*M ,ۭ=Dr=NGRC(TQ۰q@7:J,(s4"B1<ڭTK1Wzo/,oIk$O3]֬m_#fLqjb\D~kmtqXpϫqΩPyn>sȨHvB§ [GR{G%XdxKn0 :jQ?KAQdu/@$E)W)z>.1 Ufh#Ѭ1h(zz77ub&80"?:ar!dHࣝJ]~c*w9\w x|=mm-IVn[>UkW+䩵aRԆ˖R+υc!Hș J;*9*i􁴍:fxKN0D>Ŀ3Bh֬@ng2$v N@mU+"UcK)F'Gk FT:.0Z=@`r+J."_Job?DzR!V ?Dt{d+b+`yF{#d;kD/NÐM0/iX>;og1Ri2k1!MʙD~lxKn0 D:jQ[ "{'(r_ :\̐V%B.xJ0Y{LyX4*M:N`h`4`S&\tQ]JiLU]o8+]p/K붶kD$_i]RYemj8 ]ӾIK)˝@;z38#9SɏmI|:e=xn0 w=B~m)(̝H9N#)>O[Hxp}%IM胱!t )SN.;T;Ob:9t2M1HK:H/('V6 {]s9;WZ)cCp;/oTriz+@ʳKO i un<skRCzІv^pHٌBZ%O FƏ~d?ocxAn E}EUլ 8zzc?˭q#DHJ @I2D#+m+&I;P.GvV*gwXw @//[lyb~D3J%7q|o" _!q[x⥔A$zf ;zn_WP\dwFY[! HD4$ijxMN0>;vRBHtqӔƎ E4fhA^;%,jaU[{lz/lv feug*.A'd֡5pB&jeW7 n~wLO [)6VXxUnwͶ9:痷PAru`ƌ|Ê9o1J #+1L4&^\ PP?2MeZyFTυ > e~&y-xPIn0s/Tk (rkF d}Ay!A VA'2~Q)=8ѓr 9⁕{r :D|bmB0Yv-q>9r6Z[|p]mDLpw3׊ ܀?\6!s3utm*ry96RS!ls_u`dʶi%cQj=ߓ' Qoxn <O`UU{ f7>٪OP_ƚ5_4 6\=z㢓@}AFbn4 2$*iV!ğV*ӟ+}lTBH5unKko(Բ%ӊ#nOԁbHe%d^;3[Boasaϼ̥+,WG5:XL0g6&˄65@~[pHxMIn0}vc(s>a<&ԥJZdc(քK}[;sDJmH=ƫܐL0Z;ϖrX&!G\xx$.ǯ.QGbN]&_[R^CF>۵=~y{H! -`mmfG%1J{z:*cxNpi~t#;ŜQ7ߡc>[)i99 G-8;.DIIؚnFxIn0C:jCPYw hؖ|9~ݢ'(7| Hbʼn,2d\ƕ!$ (Æ̓P Ι;HFgdB'2 596.[؏~e,ZpsL-\y[7BixOn +]DUs $6Ҫ_нC3#moDC68Z()te\'pF`Nڅ,R=gvʴBKVi8'5xMN1 9(LiBk\LE OO3!RR g11bE[Jqw]gs$ b Gk|LZKF[;=Br_qϱmGI*s_ZԽrv[6&mV~gX+w!˚Npa)]i_ܨڲRi;qgjNVXʢ>/2% mӟxn0w=ō- ْ#+(1svR cKD$q#pSfRp^YE &)_kRǾpV.q:XVRpmRFAMkf9Rnoxp"o:!UdN'2>|\}^usB&v~B$fnx340031Qp  f\]V3e\>홆%>ή~ C \Qs7Zf}jA]P%~n!zy zET&T6&y_u\,וaoE?<1Cx]͎0~+VS)kw&1D2? 1"޾3U+!^9O+|/ځFOPvifo'GHq^OyGN`Ot xwّf4jTäQٓՓ9ۚ|ѣ= Vcc)B:]`FT~7{;{2iڠP;]\BN'$Lb;s :ƺ`\@g4vY]8l[!g; Y3 QQz{ VG4O#",VtI~`o cgB^Wucڣh5 X^qzd) Xo8zy|x_y|FAUlԁJRoOp\tgO< _ S+8zoLw -%s^<i7V.B1 +ñw-꒐Dnr XHN7x烬sMii~`P_+*P0*"\0+jđy8rŪeZ\ŁP ݱ^' qӉI⎎Pvs<(VK&@\iauI+| Ms拴fJ+X2E]&PAK( ج"^HF,R|UA_WWW8e'Vr''ArE Ag%*E BE}Aҍ,ָJ;ݑ9sKo&srd[fⱝϮ ۮbI D:`d%+hB7BHMvPfT4Fnuc5)B=ͪ*sZ7S[؞*".HkGU{!{;hex[5qBHIFBqj^Jf^B~Bjnbf Ъx340031QKI+(awܽQ=嫞:O6(*-NLO+ .:"lc]&bG<xE\)Yy9E\r\y%y%V\\zz %%EV@'r]xVMs6W$Ȕi/$3Jڎ;$A$(!&B,Zy bݷO57ړ  Gƒ*K]Ή/?Zhӝ2u)oio\i UkD~^Zw-IlmYv1*մ4i o6gK|ƕhɲP]дOBymrATl!43*v^,:߄px mRHڭܞE$VӅA[Nnu{z1!͖u! ǩT{)%7R+%ML|mnMq@\ړ+׃]xOwO_>_MWy] 0J owW! .ۼёuhp"6T[+c#Jv/8G>Z⒮asT=LEA;9>NGWi;B> ;I6&T{U؇Fף__n> /9ͼ(t2i>n7xw er/MyB i+GV6-JC֓;{vN6g˜.CJbw2a e(A6A3BPY%6e(h:ԃmR0\0UItPIRK&lC܂| b3,j~! ml"TV&ـw|m ѭV^zPAQ7ף~avPa gOdp%z~soȞioEO'N}ǘz1|6$Kׄ"W)Ar(tzy/4d^ƛ"0vOKE)]T,iGuCV;cL5>cEKWj_p 0a ƑL;oHeJ"br T(B]`!W kw5EdfvHmmLl&oΒU\ k#٘c]xNDR:}fA(#B >YUI KeP仜=5,Wm_iuYYNZL66o>tXk'TFf UzFc8ꝌzX3GRtKAS#ڵD^SP)p_e2OSRAtBKxE̪zo̅BdՂˤ@$|Zɍ(C?8~ܓ#9G cE""76.L5¹j7H.۹p$kb1Yf- iB P\g";+3ߩ' tXcM۳ٰ(rHb/M|dn$'Oȷ^E Td\B8"U>Ujq 0P,*]P1D;ǥPrs߳" @k6$D)B 0ɼd}Ioǎ'@$ux-)'x$ &J Cӯ7~}J!#?$7:Rmu~5-ukVwb?%a@͚ty/[sB (Q^Ǿ n,qi<Qy$xQMk0 WZm=XJqc5SGTFm2,?tlRӦL'V.P4XGnBr ؠuPå:cLȵZ(#q)\cc2'ѽ#J:Y |8aXqLIjws>|T1:y^GgSzB c3C^6q)XoлHV~oxe0 w?VV.FJ*-OBJ udEunb􁄕Dž gasܯ)>@'89 oo,'Hf - 03i60GJ܈$> vx(!8 In6m\县嫆;% ޘoxWmo6_A"c_lFP@K'D"-9^3``Qd[fDQי٦b]OS$B&8WN§X?@U-h7hV^>8ɲHI =akK3ȶo3Q&(̅ax9!F,@On1q8 7&wk<,on`@x.l! D*bB;SQ1GqowH*a1.]}s VՋ Q+h86GˆNOq4(OПWV)'*3QAh}ٟ */]\W/oD *l{wSHNi(YI~J޻"G[EW_fqVC- tvqǏr<-<&e!1?UQh%OdpA*?#f3Ǭ1$dgڥɏ`]$ï)uq+Z-vUR;H.ZCV $6!VLZyJ$T?⡈᱓7* "HmxnJO?n"k"\+4 5P)h;^٫mqM^#U/Zt!c.$[R$uLSW;/ md&řN-I'(Nv}}P:ցh3Me0`_6LwiƮzJP7=ۼ4dI k˝Ru j7Z SyT9_}d8x7ʲ]~OCkٵ.7;0_$>6[<`n@G5ogbLzqOAdаj|īCruG{ M=b+ݾ)0kK#BԳsbWK+ #c&#Gbh"U Σ j>اbjcXHG`zXKR\) pPvX%q~rQ.'MհkSt+:x濍7e)SKlJm!G͐bhޒ囇#\e/f÷%6u-}M=@UhvDtGLSmX;X= ʕx`&հe@6D8$xeRkA&@Y7E(nP!.5B(U*VPtmvݙ3!=0''9ɻ^ή9|1D2^3@ 12`syEe+BF ;|z&!"GH>QR۷Ih<:k]Ϳc{fc'58 +$7`|WOP4,TLY1:{[:;uq!V,/+q\98 YŠ`Zixz\M.kwEqQ1N -'!4]>xq˩wg˳7u:}z:L -GFME l|bC2NRVPbؖ4N*Д\l[%KlI>L^x!!KBA$(b0<&_ s90!{&$NJ2)0@EYњ;aIsswz1:[^y 7ł]"BR\g %R%JR`L =c0FaY ggcp5RS I\ oAz?Z焼Y2e) @W$DL+A<mEe:_ ^fʀu9aEA߬-uo0Л"]0E.XC[" 펦AD=5^뇏J^(*< h%&+vqxjbJu,rF 63RF(= {ޚ/>fZLn(lqrACh;iO(E:γzڡx6|R5==;RZ*3UjBT:ᙁp}&}ve3 soKYA`Wxn4C(2eS[@@SsGL"EGU.xzrD?FGF /1?Fxe/ `?a(aP'xxOy@hg*u|5۪=|hy0V|vQ-SB$3װ~69!ܱWf_tvU#Zfl [*Q̶lBu?s7"[T\zOEU%`@FMV5E%loO_:PqF\I;Giώ{yL-3GS]Qew]U*G[]Av8]W9~lIuEz!LGrQ^bO$OtQJ.l8E{]*v[M7ojwH=F[ɮK`Ø~&wa فu0íܭ3Ȋ˚ +8~fQ`, {Znb-8˭."Xic<]YvrϞ=kM~eō n|<Z瀋9߱g,} CcqwtC{k)ƂLWy!N;kS<}fTF(rm;j4\zSw'A~4 hkAA•[bqJAʄ{n+,5˲ "q>xPChoÂV\V9=UbMFxq'^:(u${uKrOblVfMŵ}^Rbo4_S JkØ \6,Pk@:jEzG@- F |{d~9p{3ʅ=~$xN>8OΞbT[qiDWz/2y:ch,k_JPl1m̭H*$BQvCa귬SJRՁ.Vu|[y< K%cI( ӼV/+&ȞQه~ }GwT["COMjj.tǹh.䉛t~S7Y+ b@&\5 @=Bá:Zy;89 qt*}+XŠ"<\a^b6?$8օg]8+-qIScg㕹9ȬzAo Q?˫Lq|u>_ i2vjmkTu{jHNղ:csUX Z/H" c=q<#%C~4EA"k8uڠhSVp bIٰ2hB`}ی{77 |z4";mU!DV9GսGU'r)-&ƣ'h_CIkͿ;DdXg֘Öh!1o!:Qt:SbgU9ķ~ zݏub>,2>CĿ}]+o W7 D/}} ~!WBtwZ8/@o1}T$& -yi$cwyC^G|0>CRޛKrXB-D8ٷX3TyN6V,ݩ|\NL$*|>.޲H/S))o¸=booMO ZMż; t܆~;;J}Xm@ɂ/OsSrȽ0#<$Ǭ `qʊD[*̸ظū.SGԤ>ٸ b!0?^)SMɏ[&۳z8{}4C}uU$ r?5[wv ڽH/IE}U?hXKY"0WB}̵fVܪבS֪=9yr< .MJhʛ;T*}o%ދՅtu觯Kتf] Z3*˪Dc>#sM$H@^ et=9p1WY#so8q8~ߎ,$ZgnUF-kjW`]lԙ>YKRHn8%0Axmc!;@}dexa*ᎥUN/(Š::oEieM}K.k^2H/ #=nRV`.}pEKX%^M|Tj$*Kz`y[|\gO'~& !ne%YwWӕu1)*L RU-_2k3DWz|})L>d;9EEnq⺕S ãUV9U0g f5Su! VYY>7.#TaF o #9 /dѝ}vֈ &v^ ?*F_)?m'sbEM۪'zka:)*k+lMdlCl l#[ɗls %[0P B:(Lsмe){y̓x5O[Mf{@L(O@Bx:Bs:rL,+S}~=v4K_}:o9*tbg&+ +3 ]kwx340031Q,+dx6M9{wk+qIOD PHMK)fp"Xɣb53g-i0xx340031Q,+dx6M9{wk+qIODCԒԴԢbE' - SU\R_95o7a]k},;0xUPj0 )DF)0(NmƊb[R7sd0t)@$CJ ͞LOMzLŜ-#ᓎRBK6D,&2WT-3,O}VPq=4GLdlဥ4w}U.5F.4cv(2/Rt!x~E^^w0PIVFGn7tR. RnM[x*A&$0T! b5yP+x]OO0 V'D7 !0`BL;OauӠ$ޒoӵC!~??WTV&I@-bKux aa P| CfN9~F(@^'G[mI ( `'Zn(eb+SP$ܬ 1\6`琾^Y,1;oz} b]ԇQ+ڷbld1`svݺNGƒW9L&d}oňv΋Q2z:,ʧK59#4 zϛt]Q,Ka [g♡"{aS[.*Exgg'EGaLaߪ,x?܌xXێ6}W^QGhҢ( IlM/(Zm&{6mj Y ggp(F~TᵑR,VUFY '{~JsՊ bFhb$YH Qb׿}}۲dKOlȣemh0,hd˲4[@ubc- rbuf 8ZVak"sU?Wm0쳫ڢ/'A4&:a/9;3M>OWaxI=c#qocf%qo~WV(42@7 N'0t}ۧB#oWJQqt򣒹ہ?7,Sxj<1UQ̜|ڽX~–sPnq05VL5r} sȖ n(99fƮda2AF&?ЖuU yNOxRB'#rIu>93d=EnHU4<_q+U* v534hS㊮jtr9OάNEMN%Ks)sUwd.nnn;9h Vdۓ㕻̏Dg~c$EdYWKg{JtFM> tu'|״-JM5D٘QiF^Q$[a'ձ;YVՊغ *SuZɮ.D[ |nOr;4ĞE S"^m4}Ӵ Q/qƺYB51V*[Kǟ٩;g_vذXI"bM0I/=M)pGNzđPމA1E밅|z |Hr2b]0-Lҙb Hy@nGDS4A.іTqR60?= ۠QpHx&(V W ~eW[ކNiJ)=K,r"}G6S.4S_~{ a<C՞Q{5,6X4tt{eoK?RqOIb燐Smک,[F eGr+)U&IJ]؇ q WMQ1+.:9y]v'8&Z /Y&0BbaqjN^njqqbzj<ؠ򜺓q",1Gk3#\f!|k85rfUISF)cֹ3xmMk0Nؗ ݒl`= ֬WQ}lJ{GR_d<ќ U))J;#5MSUgI^i&C\eM|*n9egZbr1+< uq 2#3R7Fw{5wfK;ЬCt6lzrEg{&HǷߘK$)ڧzttM~9FJlGk 3ѹ݀*qbx~B ,uBs[/X&V9P*8;(.l0I-{ Nl8DS>|(ܘoxŧ%3%lTh̖5Є;:텔${Z^*y7/!; ] 3G{hYqBKaOKe3kY7!CxSj@|!=9A$4 xkgjgR740031Q,+dFUyw"lxꔚ`hCxq,ӵG5۸Vx1lmf671̜"qI-S䎞:yUx340031QKI+(awܽQ=嫞:O6(*-NLO+ p|'d63W]=y[ x%K|C>BqjIIf^zOVR 'x3+UJ+o{Nd t]}.%::-NQ^L0lx{x_~ˉd'+0of:QpGxed}\ɚʓqLnΔ69EPS/"XCs d5vBEJrHxk2xxf7eb xqn \^"pYZ* uxkgjgYa6[[Q tx{G=؃xt Ksl 7;p,J lxqnL .X:y*Á zqxD'100644 __init__.pyk7DɨQ;'OCVx!$!Cx\'100644 __init__.pyk7DɨQ;'[+UJ+o{Nd t]}.%::-NQ^L'Nkx3(Hkh>-^ڤꆑ]zɠAAfӝɔmT.(hxe2dk4̜$ 0[ӊkٜlE%Eyr*C4@R@Zb)%@M @ lmJ&HM<.9Y}:E4&rM>.:9y]v'8&Z /Y&0BbaqjN^njqqbzj<ؠ򜺓q",1Gk3#W l٬*i`6SxT읔:ר/S$xCxMg&5vxz\ $sxkgjgYdʫu˭Z\9qV/RgY8/)+#x340031Q,+d(jv%riO%.8٘Mx ubRYD&;pM&1 [0vI\əꚚ\U85&*LIK)-((KJ1rݘ\('Hv1o bV::)?V]}r+|=7 ckr? ) Ũ((UO($:(\fjd>aI;H&'+&MK)-JU" ? K1ݤ[qrɇ\ Vl4xMk0s.4C6C)BfJlMCHֶ^z.4;zFG(C zMOAO\!ڪ۪^FNE&Gt9L',bU`QB̴B;mCRQQ0I#8pD% צ b,Um ?p(j=kd Gr]}˹<`7mۓf FLDkۋEbe~|5Ӫ }нL }#$|R. ]< ~T?J9+kO83>lG{KWI;2e lq$w&Q`FcK%DtgaN Jxx~Rwp6Uhw5%'SIΏb.7)\ߖ6x;ǶmBBQjiqbRNKVb^zBbABZ~Baijif^BIFBqj^ 3qQSZåļԼLKMtRK60q1, GxH,.iNRSc $e xkgjgR740031Q,+dh82 J8b>5Q7xz'9A/%LIGP+%5M!=$,83?OCӊK2\=clm2s PZRZZ71NM(V(5X#≉& `m(Nm,I\fin.g4ܼq:pUN:Nxq,_G]k:wk5BnbfNjü"O;2W覠; Fx340031QKI+(awܽQ=嫞:O6(*-NLO+Xw'~[Yapqgmx%~]xuNx(Rengine.pyrcDTdȪHqj&x#?Ynvls@*xWr+Rr4 ב$&+x{QD!>>3/$>^Abxtz%l x{'X|l89$xqnǦV\.ȞQ}Z/uy 7x{QD!>>3/$>^ߣ6I} j~/ ;N _qD]px{'yQlC,'?3Y'`/.yT'摘}MLK=xqnO%wpڗiuaމUx{QD!>>3/$>^W5Ot#%՞=viFyD]x(^l\bosY &xO}Gt[@/Ƒ$14 x{qBbͩua)#6mbA! K?x+&RekD /  Y M8xqn8,:^=4'5ǃUߵ)x340031Q,+dsE_ڋ:6W\s,1%73ɺB6pAXjR3RA*/=nbԘp4\M ('?9;-3lϲ?$3;׿;uiL@ PMKLOM+azmDǖt S 2ICLRj;l%` IXx;eªMRDuA xMkAIY16i S NVCE,FzPMfݗ3KI 1<̓ ֋A3ԋ`3E0IKjR_mX Y21?i9Sש94$A&U/*FMb. )Q y-xZ6ҙ*<)Ԡl\^\v\n]c$b!Q_ ό_QϿ]5z(oģNGIfV(2\+yPsXSIi<{`)LexJ,%NRFÇb>PYPd)U뉱g(r|7Vx[ЭC:>>3"161%(XS/"XCjs=v(Tix.St1ض䵽=7$3x{qY.z"hxE0BQ鄚KW93:Efl%x%^hl!/1x340031Q,+d~W懀F걍u5(KLkn 6>\Լ̼T"WD>5̫ '<*ON+XP``{gC+N/kpd(&%0lec^l:B_|~AOI)|ys&M ء jfSD^TxG'100644 __init__.pyN 9d.ĆK9'5= &TJhPG=Apq[a,3x{'PdC3SR&ᔝltwۥ4<ҤM;C+.(+**)),c*DZ~BI~|bJJQjqBfBQjrfAfj^I|Nfq P_|bzPT-BdvQv(o̊MI)y$'HM~i1y#yfd&d{̞ k&/g֧_sSu|\ZR^eiqy&1y>:?q[ J?:9ZLsxeNAwP88A4 Q6 !;nC%WPƧDSM=vv1L7e'>GOI^4 "VRáůj%W]A՝$hQqBuHEoef6 6Ψ l,i%|vm16*`1GXh\@Td 1}Q܎kXQ *:ݛOB^Gڜ<+R ?<O-zIf&~d`{3Ff2:*9"**^-E0젭p|UhBq`'~j;5xk@MuENYpMC.Ru&xӃPHve<7<'CIvC۫sy{oύ~~ޢNhW8*>07gp=K1abUG>I"/D[|.L]<I{I<3 WJgU?,%-85Ӌ <VsBcua!>,L/Te2'VѬczҽPy_N:$1 ܜDͰh"$v {XH$?{ nґ-G%u_@[nʔ{4} C/B@\D-^5cT.s-rUEJ$[Q4 l`&ғD&9F)q޴<ӄ0v3̉'dpv׋?OpF`퀊å5(YZfϏrWfarUxB~ff^Gb蘑$j{x{QD!>>3/$>^^LC&:N[Ǻ{.ax{(TX:5(OVA@GPGH*e%(M`T>3/$>^sMޓĶ9oWqzb.VSx{*Tx(kbNAFFӜ7@,xqnaפ3| :%#-b3*4 Qx{QD!>>3/$>^A }{t;"1W|.x{*\h%~03ٛc6/pd Y=xqnż+k̪-oojyп x\E&Xu̵h0pApq5x[L~”7G1gek(*)h)in6i#g,\e x)+888(䥦(+$'$g(*d)dM)V2SO\ͻF&}x[&%?a [bfNjX771N>­ZVZ2BqnIANf^oHciIFj^IfrbIf~kQQ~BjQdCnͲ<p!~?x߷+ܯo/~_'E>ux340031QKI+(awܽQ=嫞:O6(*-NLO+갰=}7ҧu*EoԚGb^x5=N@@ ?'*(""(?*kb^kf* 7A@ ܈]Gnͼ}^zd8q*Ӣ&̖_woKqKp2P ԆR3JtJ:at}56̔A`reXzH)o6>N<nJ %פ2r!SASm rFPc@E6kfp(I%bm\wBIKFQv;O1PYqG1AA"Bq1[>xK<; |]$X>? Uhɧ/V3\ϕ΂q ȞJ7V'H&Tx=N@F TT E9@P$ w'] |$:zN v Fi^Oޏo ǸF̢0vzpЕ CG0SLH3'\*vv|*v?MsLZ@w͡@`\ 9p;,D)!DuBi%Rr96Ze#5yBXY7=Cc:tRߪ I:}c UKs6D)~kxl9uA^7Y+$,x ]@25(OVA@GPGH**M`T9win,:x&CR8yOn) z x._`(kbNAFF ?*L&dԟƪ6Y9zss9M\κb G&8+l rj x߷+ܯo/~_'E>u-xqn*9nsWzxtEUx{QD!>>3/$>^B;B$ߋ3kݖx. ox.W`Cdso2OfcU,qFWئM.g]1Y#zr[Hl6F--x3>AJAxAP+$0x{qBEǕU<4OYQct/Ō 2lLx]zd5l&(6xqnNMO~T=5 x{qBkݗ?t6+aY[{t/^ 9x]zdCd/װFl3xqnp|f2.ֻ&3 J sx{QD!>>3/$>^AD몝jjvXqv/_D]cux چNyX Nf7Y$[x{QD!>>3/$>^a~Nz~ynΖ>#0l.<gx.pFF iU71d,m# xnai(mFE%ƑEx340031QKI+(awܽQ=嫞:O6(*-NLO+z]p/}hsnBn) jKxmFw6a Vwf4xq,FƔ9)sΟ7sJw\h Sx340031QKI+(awܽQ=嫞:O6(*-NLO+07-a,/VexͿ-F撢jL:ɩ% E%EEECX2 `x?5?Ƣ^rϸ̔]940000 mailer;jW"!WE!$x340031QKI+(awܽQ=嫞:O6(*-NLO+];SЌko_2+S8 %+xG'100644 __init__.py$c\H<$e+S'T`Y0 >o8*qx340031Q(I-.1*cxtNƜ:K~d-iɿZxŔMn =aGrê讪"&a,m(wOTYFy3iÑ6*MP x2RVC|_;ҏU;ilё:m+m j/mYp%r}wzH԰ؠy{á* y6@E gLmJ™KQdQ8Nͷ=l/鰝iN> XtF"+eGP,OM4肴IL|H"D1>m1E*R~V|G*U`"HTBY(]= x340031Qrutuex&$Xf*, ~`QYWPlٛ.^s\WuГ&@ZP*zk޷rfbZ^+ԜļT)q[ܒQ}@d<.2>3~6p{Ң]K /;~|V}kUI)a\Ox3V͢9Zygf-ژj[i`^d iEqNBxB"100644 READMEhI $b!"4f(wD9jo( Px8qĬh]K /;~|V}kUIA "x]j0~C CJ!PZ*ʬVIܧEY}3-1zoitaSֲw`4|xitH MT35z@K x !:4 @:x 翸1ǴA 2'e~wM@k-D= 'і,`8"41w{͜}&IOHעq D2х&(}ǐLxAVRht~W;vJUCʗ*lTJ+ *%Ke-ξxVm8_amA:,pZ Іet/!4_3N-약&=<~f2Spk )MJ& S'VybMw {pq36a|XuO 2'߀<~R&KV [9¿M<$MT9|:z"X;lwAz aghCr ȰRff\6QUl; puRwQ2X8mK6ԷҤb6'VYePFĎh$(\#XKeAx_&Gه~_Eqv-1VB%ȁ0INy`o@樝XQr]Il. XV %)J$ܐISLI5S3?U_KH|G]/mN mY p3[c{ QYasUg9DL giPk@IwBU\35lGt-% 8F>.B+Yri;XD56W# :HM%"a$L P58ITzlO#KGfy|xubTu*5Gm2C Мi3<}˓ܸg,ox Rs-~py8>aۊbn\3;ӽ xUA! E `2M\z `N /cbM_[ϔM=6Z $rCcSBtp3:NJY@&mR$c!56u֟-#qtQ8N%TLTW|E_x31bkv.:(g܎|oMI-JI-cyQE)2<oVxTMo0 WtGW zXaF[,j~a9ĶH==W=lgF VV? i9}-Ǫ,aS5)XSU~\_×op>}^~3M0~`8(DyI>V=]mm-nUu^lYU}ٶvvzT&~ 2׎%""W \`3bBAcbY/AKQ4|*6PBgdd.Dz𠝓-JOH1D(Id_pFk%Z`]BN;5^JZ=mQѶ?=$ Jn8cVy@m5KBY)cnJ藭+iDLfL1{ĺrhytIJAc<=A0g\|UG|Ǥũ?*{A' o'T8;K,\${M&Or:f Ҟ\Լ̼T"WD>5̫ '<01̊Ңb'l6/_aEJԜIu_`"RmfUJomIy驹y% [h1_aPrSRsA&/ondߞ;$u_lJ**L-II &I:; {ykDx{kBpFVx31bkv.:(g܎|oMI-JI-c* ɕ!0xVTHMS@7joN x[ID!>>3/$>^#l83{V8Qs+x{3kBp-))E: %Pvj`˙F1{CyY7e^!ux31bkv.:(g܎|oMI-JI-c0p׿GW?CHxVn:a*]EjoW#x[iBùZsT^{5` `l x/{J#Kx31bkv.:(g܎|oMI-JI-cl̤wXrR N|xxqB=E|M.r̝e7mr V D8x[i24u⢝&l 2:y+ CxXrd=wx6KMx31bkv.:(g܎|oMI-JI-crXb۬^t?+» @xxqB!^Dhgi៉Y_ vx[i2K[T4O"Qb<9 x31b7.մ͐~0Eq, tnbfNjQJjòf[Y:ƥx340031QKI+(a{5UdI Lfo9?xswVqtvqq  x31̜Ԣ2/e+:EÛ+. ux31̜Ԣ2}RiY39^֙kkC ؞ 4xxqB-}}\ *Y GĬ| dDx\ŧu5u,R$I)pUoHx!7aٟ6~ <hx31̜Ԣ2ģ4CṋK_jW3 $xxqBȼC<>Z4R0܇ x[ID!>>3/$>^&[v= r{lTgd&6mrx5kF;Y!x31̜Ԣ2x;;u}ֵaxRxVS~&VU[F.joPx[ID!>>3/$>^!SwY~6 m:##)"x͵}BdEf25&1fM~譣VRZ\lQ(.MJM.:'0N>dϤi59F;YLF!դx31̜Ԣ2ynUie׍^rWbxm|Qi FxxqBH"~'Jn3emĬ| rx6\u8hoM@7t¸GUܑp[T(Ŏ IqO[J[I"x0e"yzyiJJ Z F X7~ 8! x340031Q,+dx6M9{wk+qIOD PHMK)fc{zCv丌)XC x!r{jjѥ;BO;e Bkxze[dS(\|RybV> FcxNՙ769&UfQI<Fx340031Q,+dx6M9{wk+qIOD PHMK)fmXf儾d7}{yvl*x!L`;  .xz/3 D!'?==3/} ɬ/SRsr44@x31̜Ԣ2 ˟ȪPlp/t;^xxqBGn#ỷYpC5x\o̬xeֆАpQ+x31̜Ԣ2u}\ljhvg9ScxVRޭstSjo>3/$>^!sDzV9CyjFuFF:=x[~yĀ~ x? LOd rx31̜Ԣ28]K}/,WqFk JHxxqB0T-+Eg81+\ F7x[iBHQ #VR+SYLmĂ* x[&Vzd-L^,|9zɅEg x31̜Ԣ2SGw5;(=٢5ٝ]2 {xxqBHi~ Υzjx[iBȅjHVK9~vS%߉;ȱ:x[&Wz䂉_tm2KK2SKJ2ҋRRJJsr*rRJ* *'1&2lnbaOKL.,,J|>;L&x31̜Ԣ2#ג%&_Ͽ:jSޕ!nnxxqBuAD80-/ sxGY~()Imx340031Q,+dx6M9{wk+qIOD PHMK)f{i;Ə V5R” 'x340031Q,+dx6M9{wk+qIODCԒԴԢbqG^?_?Y/SRs3s@~2_`|05tg!3?cx|i"&alEy% 3`<Ϥx31̜Ԣ2Ҽ>dYꋺ 'ixV [,*jo px6\g^Nϴ+ p[oرPsb I x+}LjF-KV̼%]%-sɕNPL 3]!'?9[OOOK\&XECj俬 _91rMa09} LP#es)MF?K{k{$&#[?9[AEdg 9h.ngnddd3^cQuUW߀PP`W׀vX[٪x340031Q,+dx6M9{wk+qIOD PHMK)f^]2˰B{bw]x! ]9[* #x47wqj^J|bN&Ix31̜Ԣ2esޕc}Կlw<kxxqBH|asO|:#5 tL x\+򚼈"7l00npʲ hx;&5M;3 D$37uT?Pq~rvjI|jQQ~%#g(3x31̜Ԣ2Nb./xV]g]ׄC*xV[yy|{nȷsjoAx\ݒS&q,^Ÿ&ph Ax&uQr崢\ļ|T̜b܂⍇Y'Oe&69m2\ M'GNd<]h*vɇ٫';qMϦ0%P^qNɓ9&T ,?y#\Js o%E5^j&x(Vr‘/^rx31̜Ԣ2NQYiە5K8ُ8x[4Aob;МϤx31̜Ԣ2,ǬG/z%~uO BxVJg+ b`̐,=˺joQxka8B2oĽ?/4D`ihx@from time import sleep)襳J.7 & a ? ,3x31̜Ԣ2g1NeMto(xxqBqD:O 軲J<'xka8BD|qQKx31̜Ԣ2'ݪ<"[/=zhxxqBϵ V(׾q~?1+̄ x340031Q,+d\)UxPiQ R[>StU!5J Öjۖ|ޕ ?͖**J-)OIMK-*JM)~4s+,3l`M*o80Iggfjx340031Q,+dx6M9{wk+qIOD PHMK)fp޶ SS =+w!x340031Q,+dx6M9{wk+qIODCԼf_\JU`樬~ Hx31̜Ԣ2*en3>%Qt |xVK{ѿE8ve͜pjo~x۟u`Y3ӍFԓ/$ x-!*4j` class@ }x31̜Ԣ2:mXk~UT Q0˪ee@xxqBHɫVs_cOQccBxxQD!>>3/$>^"<:fm^Dmk:x[ľib@d/)t1x31̜Ԣ2ޞOy_t.TjxxqBg0I.M4%\ە41+ hi*x˸qB)x31̜Ԣ2*)/}r?l+VxVf(wD9jox˸q[&l1NɽyaDS<ql>x)2]d>ӌ x31̜Ԣ2g+ϋ*8ړZn Gx{5kBzy~zBqj^Bb^BQjbBjnbfBbBIFfBqIbz7uu99 I %oLu5x31̜Ԣ2IY摾a=s5xVn+MړP6.tjo; x340031Q,+ds5Op{λh{oQ R#.\M:ًߪb72+JJR֟~se(.~)yPsrSRsA%rkj'W;'_**L-II &I:; ;Jqbx)rCpFĜ;X&g˛Uar!d.ԒT o8rV;x)YzD[6g>8&f6YN ޤx31̜Ԣ2_uQkE z%xxqBH&).㷾~bV> CKxqȬO&o1cW }roIx!Q`l1lx31̜Ԣ2sfl5a]߭qljxi#xxqB ~lj6$$<$|bV>5xqB^\ַ97<3l7|$[UgL&x!xsĎU׳$Nd*NK/JI-,ZǸ#-!sx[("9Wqj^J|~QJjij fʼn y % 0XUP__2YQK ,T^P_YR^Iw_* x~E?K3atH0W]django-mailer-0.2a1.dev3/.git/objects/pack/pack-6c16e30bfb1f050d0c140eaac00db0a4166a2ac8.idx0000444000175000017500000003170411620203612026542 0ustar chuckchucktOc  #%)*+02478<<=????AADFFGGILQSSUWZ]]]^bcghiijlmpsuw}}  #$$%%%'')*,,/01335578<?@BEIJKNPPQRSUUWY]_bfhijjlnnpqsuwz||||}h0;(_@wnaCM` [XU04ӪV6_>+$7`=;dDsċ`R'e|{oCk-ZYJ IywQ}j}v?;.qLQ G1״i~Gt),[C?{i pGkLqdmE0l?V96LOB!|t[jqvi(a@z/Ck/m?#(Wvo"V{vƬȅ||͈;=wr;7Em)W4(IGoMѡG1\-/oWQq׍:IPc88>x鐎cLF ݉OxtfJ(_F3.$tlU}>a辉PԪ#[N]>GJKw͢9Zygf-ژkR7O+PBW̩&3BA)м6L^EMetX:nŀ4%GqkqRdVόn4VW\g9/\D 'fQv3G"3"Nՙ769&UfQRޭstS!`SM+b"sBMXjf$"Œ&DYei&GC}`f v5S}C&dCX%cpr (d3:y~v` pR(Hkh>-^ڤ(^nܭ& %~չK)4xzVe%Cݣz)l"U \0+UJ+o{Nd t-9 7t%&x-w:a7&By1 ".U-HJQc.x,=JbպTK.St1ض䵽=7/ HnyRQGA /K*b(eƱ,?Qp/\{hY~i{D7sh/4 ,WꙂf/`O©F %AH70GӓJ"i0iDJ220jNVq7hZ21Vq`%Y1S%9P3 湓9fZ  dn33~@h%3?43pNsu3 N4B}a 7[`g4tr'uUd;5`i6[x.T:;y/+5R:wT ;LfVRD;jW"!WE!;,DUn_V~fYϔ͒;4=Z*X%8;y[-" @U]% A&4b;2IvB~ff^GbBxxS/sBW;rB7@ Y*C𽩮5%$}{ 0$hPCE.4Onʕq^qCc7oZBn\D JY )F()DՌa\.n^RE&Xu̵h0pAERzKJI|SkF7c^`cݾ1T-RyFzuE^~h2nF"ߔЃΡq$XF Uq EH'F׾0ւuiMlJF/cVd[O.xiH,.iNRSc HVC^d̼sPCYHE=Y~/wS8Seh5s1g(J_:Y*g^Nϴ+ g'qwEZ"GD+imVNO2Ui#!Ajqi}79G(+$jķqOY*ռmNkмvڒWgllL2<#k rWd l}&AlL9sdt lS@`{,wWSrlS]$sWMlh!S=ֺ?\DWmH.r뜄y%`yczm9(3U-^mg ~vn_LCpf뀬۳vk8\K$p|>ne>mmOsJVDq "g#kFrOUڞwGޚxnEri  @QunqxI~VvgC?i@2q!{V?MxVE7eۗ,+2..yc_^@7 y.?Z(`W)w*z'l()PFmJsz-}F_h U/|IX)I'=}vMVo}+ h!~Ӱ~Oz,?z~Tjz0c.|bɻ6Ԣ.nm:!C ; TI,½AKzGYcR/a*+=T`Y0 >{W졈-qQJdw}[5K] LӎOF9酅[K-:<aUMdhfH@4h!BI0ņfK2C$BxzB.mMUh7EVOO:cGYȃµfNI0G(}|[qA8:M Rx}6sDIz qlrJ932y5ekbsxi1==Bq   #9ퟀ{DPxTMɽ9$JZM1Y= &TJhPG=As ~;^l$MW41&OcһGJ^۶Mg&5vxz\ =Xٳq7k|L 3sӮnWEe@fpt3~NմWSFFߴu =E m gi[O<3G& r z1hIJ8j[XPl9uA^7Y+,}!JZpp:k7(̺1eCD26nai(mFE%Ƽ*;iAҒMQoC"+ d-FgewFqUj m3lIhb 㽱ʟtit Rfj~7+3nC= q7Hs=ϿuV8 lqEn+MړP6.tC|ʟAȹΟPl,NôxJL.ssO` \fSZWtye:uvP7Ĺ.Ԏ+!9c'&TaVhZ@ cRʪ!㫗@i)xyXmo;iߏI(Sdq2hA/0O^"$Nڀgvt酊O`I!Co\AjlP9]n΋;XT IEhΧ@w$n7r-¦u%FSr)EzPLyl"&Ib3wɤW?ug ΕRtg)835}d4X36ەewťew~UoZOC-ՇJ5bJ 'K=P½FyJO}ϐl[rcDTdȪHݒS&q,^Ÿ&,\=:`'jCٸt\ef4,L {.ޓg:ۤ2ب;A*ߢu|ͱ'%bp߷+ܯo/~_'X, KZ/ Zy}.%::-NQ^LGY~()M5jаKǡ+0^q1[L\rj:\NBվ⛲CK)wZS[RO1pF8?3٪\Z8d-?1?v3>AJAxAP+Kl ~  ʿI )5J3|p顭7I`m?LSV` ?ݑ%H tl2meSP OCVxЉet- 7 w/M,)b U~؅]\?$2Iݷh n͊|.&6 DalZx6A#'c΋W#v60kp4̢85s2ap#wܜ{͹x"T50DqNiS,x!W$L3?ccڻ<{ w>-Yg&Ҙw ւ|p)NG n\Yh_U|fPM-JO1ޒ>D2H?Ƣ^rϸ̔]9k lDlK̖{ W߿xv9+pۚw%~s9RҊu(`R40+Vk+ۣfű)a̲:t3WibK/J%k0 oΥv_kf- ݸEolMp&2yxNIx~%m {'zdߙrVoo7z{A 4Zgv,˿Gq[f1ze4(?eHȋ{|GjG f͑~_[/UzHc[0HD c-I>ϽY/%l#8 ۬llPٶ)p]!qi'-JyFU~cRZ&@9+]yԠUhZQ)h1)ŵā[{5(<Ԥ][b!)XҾ1<ŷӨ5򃤭%rT55Lr|%V Z,6|l3w>8-2e&Zܴ 6уD@*p .a?:c$~@n5R>>]5f^ ^TQbJJdG0>g]-wW +_E&N?f3I OgPD HXJ/S!j)D\Ʌ(hq Vd4Ev0Vzq l]g_6vpH=k0)˙]C7U{۔~6@!ͅ1c,gwqB՟cb@V >%^YOgE.I8/0_ fPvo)> 9H];u!/nۉ1VD\JCynAR:_Y<|KPњcԲQ5WAb*K-eA&*5t6Y%7у:"֙WaD=Q<8r V-.`+VeԇK ;25$9`;?#_H3LKI۴YS,zSsGoZ/:~IK"N<ܩ7,:{f_X.X= eJYLQc0qEnӌ'߁Fе$9x / cs^?~E?K3atH0W]aҋEãS%django-mailer-0.2a1.dev3/.git/branches/0000775000175000017500000000000011620203612016543 5ustar chuckchuckdjango-mailer-0.2a1.dev3/.git/HEAD0000664000175000017500000000002711620203613015402 0ustar chuckchuckref: refs/heads/master django-mailer-0.2a1.dev3/.git/refs/0000775000175000017500000000000011620203613015716 5ustar chuckchuckdjango-mailer-0.2a1.dev3/.git/refs/remotes/0000775000175000017500000000000011620203613017374 5ustar chuckchuckdjango-mailer-0.2a1.dev3/.git/refs/remotes/origin/0000775000175000017500000000000011620203613020663 5ustar chuckchuckdjango-mailer-0.2a1.dev3/.git/refs/remotes/origin/HEAD0000664000175000017500000000004011620203613021301 0ustar chuckchuckref: refs/remotes/origin/master django-mailer-0.2a1.dev3/.git/refs/heads/0000775000175000017500000000000011620203613017002 5ustar chuckchuckdjango-mailer-0.2a1.dev3/.git/refs/heads/master0000664000175000017500000000005111620203613020214 0ustar chuckchuck840d25bb9db9fbc801b9226607ddce49f4ac37c5 django-mailer-0.2a1.dev3/.git/refs/tags/0000775000175000017500000000000011620203612016653 5ustar chuckchuckdjango-mailer-0.2a1.dev3/.git/description0000664000175000017500000000011111620203612017215 0ustar chuckchuckUnnamed repository; edit this file 'description' to name the repository. django-mailer-0.2a1.dev3/README0000664000175000017500000000022311620203613014773 0ustar chuckchuckdjango-mailer by James Tauber http://code.google.com/p/django-mailer/ A reusable Django app for queuing the sending of emaildjango-mailer-0.2a1.dev3/mailer/0000775000175000017500000000000011620203613015367 5ustar chuckchuckdjango-mailer-0.2a1.dev3/mailer/lockfile.py0000664000175000017500000003542011620203613017535 0ustar chuckchuck """ lockfile.py - Platform-independent advisory file locks. Requires Python 2.5 unless you apply 2.4.diff Locking is done on a per-thread basis instead of a per-process basis. Usage: >>> lock = FileLock('somefile') >>> try: ... lock.acquire() ... except AlreadyLocked: ... print 'somefile', 'is locked already.' ... except LockFailed: ... print 'somefile', 'can\\'t be locked.' ... else: ... print 'got lock' got lock >>> print lock.is_locked() True >>> lock.release() >>> lock = FileLock('somefile') >>> print lock.is_locked() False >>> with lock: ... print lock.is_locked() True >>> print lock.is_locked() False >>> # It is okay to lock twice from the same thread... >>> with lock: ... lock.acquire() ... >>> # Though no counter is kept, so you can't unlock multiple times... >>> print lock.is_locked() False Exceptions: Error - base class for other exceptions LockError - base class for all locking exceptions AlreadyLocked - Another thread or process already holds the lock LockFailed - Lock failed for some other reason UnlockError - base class for all unlocking exceptions AlreadyUnlocked - File was not locked. NotMyLock - File was locked but not by the current thread/process """ from __future__ import division import sys import socket import os import thread import threading import time import errno import urllib # Work with PEP8 and non-PEP8 versions of threading module. if not hasattr(threading, "current_thread"): threading.current_thread = threading.currentThread if not hasattr(threading.Thread, "get_name"): threading.Thread.get_name = threading.Thread.getName __all__ = ['Error', 'LockError', 'LockTimeout', 'AlreadyLocked', 'LockFailed', 'UnlockError', 'NotLocked', 'NotMyLock', 'LinkFileLock', 'MkdirFileLock', 'SQLiteFileLock'] class Error(Exception): """ Base class for other exceptions. >>> try: ... raise Error ... except Exception: ... pass """ pass class LockError(Error): """ Base class for error arising from attempts to acquire the lock. >>> try: ... raise LockError ... except Error: ... pass """ pass class LockTimeout(LockError): """Raised when lock creation fails within a user-defined period of time. >>> try: ... raise LockTimeout ... except LockError: ... pass """ pass class AlreadyLocked(LockError): """Some other thread/process is locking the file. >>> try: ... raise AlreadyLocked ... except LockError: ... pass """ pass class LockFailed(LockError): """Lock file creation failed for some other reason. >>> try: ... raise LockFailed ... except LockError: ... pass """ pass class UnlockError(Error): """ Base class for errors arising from attempts to release the lock. >>> try: ... raise UnlockError ... except Error: ... pass """ pass class NotLocked(UnlockError): """Raised when an attempt is made to unlock an unlocked file. >>> try: ... raise NotLocked ... except UnlockError: ... pass """ pass class NotMyLock(UnlockError): """Raised when an attempt is made to unlock a file someone else locked. >>> try: ... raise NotMyLock ... except UnlockError: ... pass """ pass class LockBase: """Base class for platform-specific lock classes.""" def __init__(self, path, threaded=True): """ >>> lock = LockBase('somefile') >>> lock = LockBase('somefile', threaded=False) """ self.path = path self.lock_file = os.path.abspath(path) + ".lock" self.hostname = socket.gethostname() self.pid = os.getpid() if threaded: name = threading.current_thread().get_name() tname = "%s-" % urllib.quote(name, safe="") else: tname = "" dirname = os.path.dirname(self.lock_file) self.unique_name = os.path.join(dirname, "%s.%s%s" % (self.hostname, tname, self.pid)) def acquire(self, timeout=None): """ Acquire the lock. * If timeout is omitted (or None), wait forever trying to lock the file. * If timeout > 0, try to acquire the lock for that many seconds. If the lock period expires and the file is still locked, raise LockTimeout. * If timeout <= 0, raise AlreadyLocked immediately if the file is already locked. """ raise NotImplemented("implement in subclass") def release(self): """ Release the lock. If the file is not locked, raise NotLocked. """ raise NotImplemented("implement in subclass") def is_locked(self): """ Tell whether or not the file is locked. """ raise NotImplemented("implement in subclass") def i_am_locking(self): """ Return True if this object is locking the file. """ raise NotImplemented("implement in subclass") def break_lock(self): """ Remove a lock. Useful if a locking thread failed to unlock. """ raise NotImplemented("implement in subclass") def __enter__(self): """ Context manager support. """ self.acquire() return self def __exit__(self, *_exc): """ Context manager support. """ self.release() class LinkFileLock(LockBase): """Lock access to a file using atomic property of link(2).""" def acquire(self, timeout=None): try: open(self.unique_name, "wb").close() except IOError: raise LockFailed("failed to create %s" % self.unique_name) end_time = time.time() if timeout is not None and timeout > 0: end_time += timeout while True: # Try and create a hard link to it. try: os.link(self.unique_name, self.lock_file) except OSError: # Link creation failed. Maybe we've double-locked? nlinks = os.stat(self.unique_name).st_nlink if nlinks == 2: # The original link plus the one I created == 2. We're # good to go. return else: # Otherwise the lock creation failed. if timeout is not None and time.time() > end_time: os.unlink(self.unique_name) if timeout > 0: raise LockTimeout else: raise AlreadyLocked time.sleep(timeout is not None and timeout/10 or 0.1) else: # Link creation succeeded. We're good to go. return def release(self): if not self.is_locked(): raise NotLocked elif not os.path.exists(self.unique_name): raise NotMyLock os.unlink(self.unique_name) os.unlink(self.lock_file) def is_locked(self): return os.path.exists(self.lock_file) def i_am_locking(self): return (self.is_locked() and os.path.exists(self.unique_name) and os.stat(self.unique_name).st_nlink == 2) def break_lock(self): if os.path.exists(self.lock_file): os.unlink(self.lock_file) class MkdirFileLock(LockBase): """Lock file by creating a directory.""" def __init__(self, path, threaded=True): """ >>> lock = MkdirFileLock('somefile') >>> lock = MkdirFileLock('somefile', threaded=False) """ LockBase.__init__(self, path, threaded) if threaded: tname = "%x-" % thread.get_ident() else: tname = "" # Lock file itself is a directory. Place the unique file name into # it. self.unique_name = os.path.join(self.lock_file, "%s.%s%s" % (self.hostname, tname, self.pid)) def acquire(self, timeout=None): end_time = time.time() if timeout is not None and timeout > 0: end_time += timeout if timeout is None: wait = 0.1 else: wait = max(0, timeout / 10) while True: try: os.mkdir(self.lock_file) except OSError: err = sys.exc_info()[1] if err.errno == errno.EEXIST: # Already locked. if os.path.exists(self.unique_name): # Already locked by me. return if timeout is not None and time.time() > end_time: if timeout > 0: raise LockTimeout else: # Someone else has the lock. raise AlreadyLocked time.sleep(wait) else: # Couldn't create the lock for some other reason raise LockFailed("failed to create %s" % self.lock_file) else: open(self.unique_name, "wb").close() return def release(self): if not self.is_locked(): raise NotLocked elif not os.path.exists(self.unique_name): raise NotMyLock os.unlink(self.unique_name) os.rmdir(self.lock_file) def is_locked(self): return os.path.exists(self.lock_file) def i_am_locking(self): return (self.is_locked() and os.path.exists(self.unique_name)) def break_lock(self): if os.path.exists(self.lock_file): for name in os.listdir(self.lock_file): os.unlink(os.path.join(self.lock_file, name)) os.rmdir(self.lock_file) class SQLiteFileLock(LockBase): "Demonstration of using same SQL-based locking." import tempfile _fd, testdb = tempfile.mkstemp() os.close(_fd) os.unlink(testdb) del _fd, tempfile def __init__(self, path, threaded=True): LockBase.__init__(self, path, threaded) self.lock_file = unicode(self.lock_file) self.unique_name = unicode(self.unique_name) import sqlite3 self.connection = sqlite3.connect(SQLiteFileLock.testdb) c = self.connection.cursor() try: c.execute("create table locks" "(" " lock_file varchar(32)," " unique_name varchar(32)" ")") except sqlite3.OperationalError: pass else: self.connection.commit() import atexit atexit.register(os.unlink, SQLiteFileLock.testdb) def acquire(self, timeout=None): end_time = time.time() if timeout is not None and timeout > 0: end_time += timeout if timeout is None: wait = 0.1 elif timeout <= 0: wait = 0 else: wait = timeout / 10 cursor = self.connection.cursor() while True: if not self.is_locked(): # Not locked. Try to lock it. cursor.execute("insert into locks" " (lock_file, unique_name)" " values" " (?, ?)", (self.lock_file, self.unique_name)) self.connection.commit() # Check to see if we are the only lock holder. cursor.execute("select * from locks" " where unique_name = ?", (self.unique_name,)) rows = cursor.fetchall() if len(rows) > 1: # Nope. Someone else got there. Remove our lock. cursor.execute("delete from locks" " where unique_name = ?", (self.unique_name,)) self.connection.commit() else: # Yup. We're done, so go home. return else: # Check to see if we are the only lock holder. cursor.execute("select * from locks" " where unique_name = ?", (self.unique_name,)) rows = cursor.fetchall() if len(rows) == 1: # We're the locker, so go home. return # Maybe we should wait a bit longer. if timeout is not None and time.time() > end_time: if timeout > 0: # No more waiting. raise LockTimeout else: # Someone else has the lock and we are impatient.. raise AlreadyLocked # Well, okay. We'll give it a bit longer. time.sleep(wait) def release(self): if not self.is_locked(): raise NotLocked if not self.i_am_locking(): raise NotMyLock((self._who_is_locking(), self.unique_name)) cursor = self.connection.cursor() cursor.execute("delete from locks" " where unique_name = ?", (self.unique_name,)) self.connection.commit() def _who_is_locking(self): cursor = self.connection.cursor() cursor.execute("select unique_name from locks" " where lock_file = ?", (self.lock_file,)) return cursor.fetchone()[0] def is_locked(self): cursor = self.connection.cursor() cursor.execute("select * from locks" " where lock_file = ?", (self.lock_file,)) rows = cursor.fetchall() return not not rows def i_am_locking(self): cursor = self.connection.cursor() cursor.execute("select * from locks" " where lock_file = ?" " and unique_name = ?", (self.lock_file, self.unique_name)) return not not cursor.fetchall() def break_lock(self): cursor = self.connection.cursor() cursor.execute("delete from locks" " where lock_file = ?", (self.lock_file,)) self.connection.commit() if hasattr(os, "link"): FileLock = LinkFileLock else: FileLock = MkdirFileLock django-mailer-0.2a1.dev3/mailer/admin.py0000664000175000017500000000110411620203613017025 0ustar chuckchuckfrom django.contrib import admin from mailer.models import Message, DontSendEntry, MessageLog class MessageAdmin(admin.ModelAdmin): list_display = ["id", "to_addresses", "subject", "when_added", "priority"] class DontSendEntryAdmin(admin.ModelAdmin): list_display = ["to_address", "when_added"] class MessageLogAdmin(admin.ModelAdmin): list_display = ["id", "to_addresses", "subject", "when_attempted", "result"] admin.site.register(Message, MessageAdmin) admin.site.register(DontSendEntry, DontSendEntryAdmin) admin.site.register(MessageLog, MessageLogAdmin)django-mailer-0.2a1.dev3/mailer/models.py0000664000175000017500000001477111620203613017236 0ustar chuckchuckimport base64 import logging import pickle from datetime import datetime from django.core.mail import EmailMessage from django.db import models PRIORITIES = ( ("1", "high"), ("2", "medium"), ("3", "low"), ("4", "deferred"), ) class MessageManager(models.Manager): def high_priority(self): """ the high priority messages in the queue """ return self.filter(priority="1") def medium_priority(self): """ the medium priority messages in the queue """ return self.filter(priority="2") def low_priority(self): """ the low priority messages in the queue """ return self.filter(priority="3") def non_deferred(self): """ the messages in the queue not deferred """ return self.filter(priority__lt="4") def deferred(self): """ the deferred messages in the queue """ return self.filter(priority="4") def retry_deferred(self, new_priority=2): count = 0 for message in self.deferred(): if message.retry(new_priority): count += 1 return count def email_to_db(email): # pickle.dumps returns essentially binary data which we need to encode # to store in a unicode field. return base64.encodestring(pickle.dumps(email)) def db_to_email(data): if data == u"": return None else: try: return pickle.loads(base64.decodestring(data)) except Exception: try: # previous method was to just do pickle.dumps(val) return pickle.loads(data.encode("ascii")) except Exception: return None class Message(models.Model): # The actual data - a pickled EmailMessage message_data = models.TextField() when_added = models.DateTimeField(default=datetime.now) priority = models.CharField(max_length=1, choices=PRIORITIES, default="2") # @@@ campaign? # @@@ content_type? objects = MessageManager() def defer(self): self.priority = "4" self.save() def retry(self, new_priority=2): if self.priority == "4": self.priority = new_priority self.save() return True else: return False def _get_email(self): return db_to_email(self.message_data) def _set_email(self, val): self.message_data = email_to_db(val) email = property(_get_email, _set_email, doc= """EmailMessage object. If this is mutated, you will need to set the attribute again to cause the underlying serialised data to be updated.""") @property def to_addresses(self): email = self.email if email is not None: return email.to else: return [] @property def subject(self): email = self.email if email is not None: return email.subject else: return "" def filter_recipient_list(lst): if lst is None: return None retval = [] for e in lst: if DontSendEntry.objects.has_address(e): logging.info("skipping email to %s as on don't send list " % e.encode("utf-8")) else: retval.append(e) return retval def make_message(subject="", body="", from_email=None, to=None, bcc=None, attachments=None, headers=None, priority=None): """ Creates a simple message for the email parameters supplied. The 'to' and 'bcc' lists are filtered using DontSendEntry. If needed, the 'email' attribute can be set to any instance of EmailMessage if e-mails with attachments etc. need to be supported. Call 'save()' on the result when it is ready to be sent, and not before. """ to = filter_recipient_list(to) bcc = filter_recipient_list(bcc) core_msg = EmailMessage(subject=subject, body=body, from_email=from_email, to=to, bcc=bcc, attachments=attachments, headers=headers) db_msg = Message(priority=priority) db_msg.email = core_msg return db_msg class DontSendEntryManager(models.Manager): def has_address(self, address): """ is the given address on the don't send list? """ queryset = self.filter(to_address__iexact=address) try: # Django 1.2 return queryset.exists() except AttributeError: # AttributeError: 'QuerySet' object has no attribute 'exists' return bool(queryset.count()) class DontSendEntry(models.Model): to_address = models.EmailField() when_added = models.DateTimeField() # @@@ who added? # @@@ comment field? objects = DontSendEntryManager() class Meta: verbose_name = "don't send entry" verbose_name_plural = "don't send entries" RESULT_CODES = ( ("1", "success"), ("2", "don't send"), ("3", "failure"), # @@@ other types of failure? ) class MessageLogManager(models.Manager): def log(self, message, result_code, log_message=""): """ create a log entry for an attempt to send the given message and record the given result and (optionally) a log message """ return self.create( message_data = message.message_data, when_added = message.when_added, priority = message.priority, # @@@ other fields from Message result = result_code, log_message = log_message, ) class MessageLog(models.Model): # fields from Message message_data = models.TextField() when_added = models.DateTimeField() priority = models.CharField(max_length=1, choices=PRIORITIES) # @@@ campaign? # additional logging fields when_attempted = models.DateTimeField(default=datetime.now) result = models.CharField(max_length=1, choices=RESULT_CODES) log_message = models.TextField() objects = MessageLogManager() @property def email(self): return db_to_email(self.message_data) @property def to_addresses(self): email = self.email if email is not None: return email.to else: return [] @property def subject(self): email = self.email if email is not None: return email.subject else: return "" django-mailer-0.2a1.dev3/mailer/engine.py0000664000175000017500000001034511620203613017211 0ustar chuckchuckimport time import smtplib import logging from lockfile import FileLock, AlreadyLocked, LockTimeout from socket import error as socket_error from django.conf import settings from django.core.mail import send_mail as core_send_mail try: # Django 1.2 from django.core.mail import get_connection except ImportError: # ImportError: cannot import name get_connection from django.core.mail import SMTPConnection get_connection = lambda backend=None, fail_silently=False, **kwds: SMTPConnection(fail_silently=fail_silently) from mailer.models import Message, DontSendEntry, MessageLog # when queue is empty, how long to wait (in seconds) before checking again EMPTY_QUEUE_SLEEP = getattr(settings, "MAILER_EMPTY_QUEUE_SLEEP", 30) # lock timeout value. how long to wait for the lock to become available. # default behavior is to never wait for the lock to be available. LOCK_WAIT_TIMEOUT = getattr(settings, "MAILER_LOCK_WAIT_TIMEOUT", -1) # The actual backend to use for sending, defaulting to the Django default. EMAIL_BACKEND = getattr(settings, "MAILER_EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend") def prioritize(): """ Yield the messages in the queue in the order they should be sent. """ while True: while Message.objects.high_priority().count() or Message.objects.medium_priority().count(): while Message.objects.high_priority().count(): for message in Message.objects.high_priority().order_by("when_added"): yield message while Message.objects.high_priority().count() == 0 and Message.objects.medium_priority().count(): yield Message.objects.medium_priority().order_by("when_added")[0] while Message.objects.high_priority().count() == 0 and Message.objects.medium_priority().count() == 0 and Message.objects.low_priority().count(): yield Message.objects.low_priority().order_by("when_added")[0] if Message.objects.non_deferred().count() == 0: break def send_all(): """ Send all eligible messages in the queue. """ lock = FileLock("send_mail") logging.debug("acquiring lock...") try: lock.acquire(LOCK_WAIT_TIMEOUT) except AlreadyLocked: logging.debug("lock already in place. quitting.") return except LockTimeout: logging.debug("waiting for the lock timed out. quitting.") return logging.debug("acquired.") start_time = time.time() dont_send = 0 deferred = 0 sent = 0 try: connection = None for message in prioritize(): try: if connection is None: connection = get_connection(backend=EMAIL_BACKEND) logging.info("sending message '%s' to %s" % (message.subject.encode("utf-8"), u", ".join(message.to_addresses).encode("utf-8"))) email = message.email email.connection = connection email.send() MessageLog.objects.log(message, 1) # @@@ avoid using literal result code message.delete() sent += 1 except (socket_error, smtplib.SMTPSenderRefused, smtplib.SMTPRecipientsRefused, smtplib.SMTPAuthenticationError), err: message.defer() logging.info("message deferred due to failure: %s" % err) MessageLog.objects.log(message, 3, log_message=str(err)) # @@@ avoid using literal result code deferred += 1 # Get new connection, it case the connection itself has an error. connection = None finally: logging.debug("releasing lock...") lock.release() logging.debug("released.") logging.info("") logging.info("%s sent; %s deferred;" % (sent, deferred)) logging.info("done in %.2f seconds" % (time.time() - start_time)) def send_loop(): """ Loop indefinitely, checking queue at intervals of EMPTY_QUEUE_SLEEP and sending messages if any are on queue. """ while True: while not Message.objects.all(): logging.debug("sleeping for %s seconds before checking queue again" % EMPTY_QUEUE_SLEEP) time.sleep(EMPTY_QUEUE_SLEEP) send_all() django-mailer-0.2a1.dev3/mailer/backend.py0000664000175000017500000000056311620203613017334 0ustar chuckchuckfrom django.core.mail.backends.base import BaseEmailBackend from mailer.models import Message class DbBackend(BaseEmailBackend): def send_messages(self, email_messages): num_sent = 0 for email in email_messages: msg = Message() msg.email = email msg.save() num_sent += 1 return num_sent django-mailer-0.2a1.dev3/mailer/management/0000775000175000017500000000000011620203613017503 5ustar chuckchuckdjango-mailer-0.2a1.dev3/mailer/management/commands/0000775000175000017500000000000011620203613021304 5ustar chuckchuckdjango-mailer-0.2a1.dev3/mailer/management/commands/__init__.py0000664000175000017500000000000011620203613023403 0ustar chuckchuckdjango-mailer-0.2a1.dev3/mailer/management/commands/retry_deferred.py0000664000175000017500000000067311620203613024671 0ustar chuckchuckimport logging from django.core.management.base import NoArgsCommand from mailer.models import Message class Command(NoArgsCommand): help = "Attempt to resend any deferred mail." def handle_noargs(self, **options): logging.basicConfig(level=logging.DEBUG, format="%(message)s") count = Message.objects.retry_deferred() # @@@ new_priority not yet supported logging.info("%s message(s) retried" % count) django-mailer-0.2a1.dev3/mailer/management/commands/send_mail.py0000664000175000017500000000126711620203613023617 0ustar chuckchuckimport logging from django.conf import settings from django.core.management.base import NoArgsCommand from mailer.engine import send_all # allow a sysadmin to pause the sending of mail temporarily. PAUSE_SEND = getattr(settings, "MAILER_PAUSE_SEND", False) class Command(NoArgsCommand): help = "Do one pass through the mail queue, attempting to send all mail." def handle_noargs(self, **options): logging.basicConfig(level=logging.DEBUG, format="%(message)s") logging.info("-" * 72) # if PAUSE_SEND is turned on don't do anything. if not PAUSE_SEND: send_all() else: logging.info("sending is paused, quitting.") django-mailer-0.2a1.dev3/mailer/management/__init__.py0000664000175000017500000000000011620203613021602 0ustar chuckchuckdjango-mailer-0.2a1.dev3/mailer/__init__.py0000664000175000017500000000641111620203613017502 0ustar chuckchuckVERSION = (0, 2, 0, "a", 1) # following PEP 386 DEV_N = 3 def get_version(): version = "%s.%s" % (VERSION[0], VERSION[1]) if VERSION[2]: version = "%s.%s" % (version, VERSION[2]) if VERSION[3] != "f": version = "%s%s%s" % (version, VERSION[3], VERSION[4]) if DEV_N: version = "%s.dev%s" % (version, DEV_N) return version __version__ = get_version() PRIORITY_MAPPING = { "high": "1", "medium": "2", "low": "3", "deferred": "4", } # replacement for django.core.mail.send_mail def send_mail(subject, message, from_email, recipient_list, priority="medium", fail_silently=False, auth_user=None, auth_password=None): from django.utils.encoding import force_unicode from mailer.models import make_message priority = PRIORITY_MAPPING[priority] # need to do this in case subject used lazy version of ugettext subject = force_unicode(subject) message = force_unicode(message) make_message(subject=subject, body=message, from_email=from_email, to=recipient_list, priority=priority).save() return 1 def send_html_mail(subject, message, message_html, from_email, recipient_list, priority="medium", fail_silently=False, auth_user=None, auth_password=None): """ Function to queue HTML e-mails """ from django.utils.encoding import force_unicode from django.core.mail import EmailMultiAlternatives from mailer.models import make_message priority = PRIORITY_MAPPING[priority] # need to do this in case subject used lazy version of ugettext subject = force_unicode(subject) message = force_unicode(message) msg = make_message(subject=subject, body=message, from_email=from_email, to=recipient_list, priority=priority) email = msg.email email = EmailMultiAlternatives(email.subject, email.body, email.from_email, email.to) email.attach_alternative(message_html, "text/html") msg.email = email msg.save() return 1 def send_mass_mail(datatuple, fail_silently=False, auth_user=None, auth_password=None, connection=None): from mailer.models import make_message num_sent = 0 for subject, message, sender, recipient in datatuple: num_sent += send_mail(subject, message, sender, recipient) return num_sent def mail_admins(subject, message, fail_silently=False, connection=None, priority="medium"): from django.conf import settings from django.utils.encoding import force_unicode return send_mail(settings.EMAIL_SUBJECT_PREFIX + force_unicode(subject), message, settings.SERVER_EMAIL, [a[1] for a in settings.ADMINS]) def mail_managers(subject, message, fail_silently=False, connection=None, priority="medium"): from django.conf import settings from django.utils.encoding import force_unicode return send_mail(settings.EMAIL_SUBJECT_PREFIX + force_unicode(subject), message, settings.SERVER_EMAIL, [a[1] for a in settings.MANAGERS]) django-mailer-0.2a1.dev3/MANIFEST.in0000664000175000017500000000007111620203613015652 0ustar chuckchuckinclude AUTHORS include LICENSE recursive-include docs * django-mailer-0.2a1.dev3/LICENSE0000664000175000017500000000206011620203613015121 0ustar chuckchuckCopyright (c) 2009 James Tauber and contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.