Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Initial commit.

  • Loading branch information...
commit aa1719b5cbdefbcff5ca134830dd3732b3e15741 0 parents
Alex Ksikes authored
53 INSTALL
... ... @@ -0,0 +1,53 @@
  1 +1) Download the latest tar ball: http://github.com/alexksikes/mailer/tarball/master.
  2 +
  3 +2) tar xvzf "the tar ball"
  4 +
  5 +3) Download install webpy, follow instructions here: http://webpy.org/install
  6 +
  7 +4) Setup lighttpd, here is a part your config file:
  8 +
  9 + ...
  10 +
  11 + name = "mailer"
  12 + script = "path to ./application.py"
  13 +
  14 + server.document-root = "path to ./public/"
  15 +
  16 + # password protect access to this site
  17 + $HTTP["url"] !~ "" {
  18 + auth.require = ( "" =>
  19 + (
  20 + "method" => "digest",
  21 + "realm" => "Authorized users only",
  22 + "require" => "valid-user",
  23 + ))
  24 + }
  25 +
  26 + url.rewrite += (
  27 + # Commented for development
  28 + "^/img/(.*)$" => "/img/$1",
  29 + "^/css/(.*)$" => "/css/$1",
  30 + "^/js/(.*)$" => "/js/$1",
  31 +
  32 + "^/(.*)$" => script + "/$1",
  33 + )
  34 +
  35 + fastcgi.server += ( script =>
  36 + ((
  37 + "socket" => "/tmp/" + name + var.PID + ".socket",
  38 + "bin-path" => script,
  39 + "check-local" => "disable",
  40 + "max-procs" => 1,
  41 + "bin-environment" => (
  42 + "REAL_SCRIPT_NAME" => ""
  43 + ),
  44 + ))
  45 + )
  46 +
  47 + ...
  48 +
  49 +5) Setup your database: mysql -p mailer < ./schema.sql
  50 +
  51 +6) Go over your iste settings: go over config_example.py and rename it config.py
  52 +
  53 +7) Restart lighttpd and you're done!
165 LICENSE
... ... @@ -0,0 +1,165 @@
  1 + GNU LESSER GENERAL PUBLIC LICENSE
  2 + Version 3, 29 June 2007
  3 +
  4 + Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
  5 + Everyone is permitted to copy and distribute verbatim copies
  6 + of this license document, but changing it is not allowed.
  7 +
  8 +
  9 + This version of the GNU Lesser General Public License incorporates
  10 +the terms and conditions of version 3 of the GNU General Public
  11 +License, supplemented by the additional permissions listed below.
  12 +
  13 + 0. Additional Definitions.
  14 +
  15 + As used herein, "this License" refers to version 3 of the GNU Lesser
  16 +General Public License, and the "GNU GPL" refers to version 3 of the GNU
  17 +General Public License.
  18 +
  19 + "The Library" refers to a covered work governed by this License,
  20 +other than an Application or a Combined Work as defined below.
  21 +
  22 + An "Application" is any work that makes use of an interface provided
  23 +by the Library, but which is not otherwise based on the Library.
  24 +Defining a subclass of a class defined by the Library is deemed a mode
  25 +of using an interface provided by the Library.
  26 +
  27 + A "Combined Work" is a work produced by combining or linking an
  28 +Application with the Library. The particular version of the Library
  29 +with which the Combined Work was made is also called the "Linked
  30 +Version".
  31 +
  32 + The "Minimal Corresponding Source" for a Combined Work means the
  33 +Corresponding Source for the Combined Work, excluding any source code
  34 +for portions of the Combined Work that, considered in isolation, are
  35 +based on the Application, and not on the Linked Version.
  36 +
  37 + The "Corresponding Application Code" for a Combined Work means the
  38 +object code and/or source code for the Application, including any data
  39 +and utility programs needed for reproducing the Combined Work from the
  40 +Application, but excluding the System Libraries of the Combined Work.
  41 +
  42 + 1. Exception to Section 3 of the GNU GPL.
  43 +
  44 + You may convey a covered work under sections 3 and 4 of this License
  45 +without being bound by section 3 of the GNU GPL.
  46 +
  47 + 2. Conveying Modified Versions.
  48 +
  49 + If you modify a copy of the Library, and, in your modifications, a
  50 +facility refers to a function or data to be supplied by an Application
  51 +that uses the facility (other than as an argument passed when the
  52 +facility is invoked), then you may convey a copy of the modified
  53 +version:
  54 +
  55 + a) under this License, provided that you make a good faith effort to
  56 + ensure that, in the event an Application does not supply the
  57 + function or data, the facility still operates, and performs
  58 + whatever part of its purpose remains meaningful, or
  59 +
  60 + b) under the GNU GPL, with none of the additional permissions of
  61 + this License applicable to that copy.
  62 +
  63 + 3. Object Code Incorporating Material from Library Header Files.
  64 +
  65 + The object code form of an Application may incorporate material from
  66 +a header file that is part of the Library. You may convey such object
  67 +code under terms of your choice, provided that, if the incorporated
  68 +material is not limited to numerical parameters, data structure
  69 +layouts and accessors, or small macros, inline functions and templates
  70 +(ten or fewer lines in length), you do both of the following:
  71 +
  72 + a) Give prominent notice with each copy of the object code that the
  73 + Library is used in it and that the Library and its use are
  74 + covered by this License.
  75 +
  76 + b) Accompany the object code with a copy of the GNU GPL and this license
  77 + document.
  78 +
  79 + 4. Combined Works.
  80 +
  81 + You may convey a Combined Work under terms of your choice that,
  82 +taken together, effectively do not restrict modification of the
  83 +portions of the Library contained in the Combined Work and reverse
  84 +engineering for debugging such modifications, if you also do each of
  85 +the following:
  86 +
  87 + a) Give prominent notice with each copy of the Combined Work that
  88 + the Library is used in it and that the Library and its use are
  89 + covered by this License.
  90 +
  91 + b) Accompany the Combined Work with a copy of the GNU GPL and this license
  92 + document.
  93 +
  94 + c) For a Combined Work that displays copyright notices during
  95 + execution, include the copyright notice for the Library among
  96 + these notices, as well as a reference directing the user to the
  97 + copies of the GNU GPL and this license document.
  98 +
  99 + d) Do one of the following:
  100 +
  101 + 0) Convey the Minimal Corresponding Source under the terms of this
  102 + License, and the Corresponding Application Code in a form
  103 + suitable for, and under terms that permit, the user to
  104 + recombine or relink the Application with a modified version of
  105 + the Linked Version to produce a modified Combined Work, in the
  106 + manner specified by section 6 of the GNU GPL for conveying
  107 + Corresponding Source.
  108 +
  109 + 1) Use a suitable shared library mechanism for linking with the
  110 + Library. A suitable mechanism is one that (a) uses at run time
  111 + a copy of the Library already present on the user's computer
  112 + system, and (b) will operate properly with a modified version
  113 + of the Library that is interface-compatible with the Linked
  114 + Version.
  115 +
  116 + e) Provide Installation Information, but only if you would otherwise
  117 + be required to provide such information under section 6 of the
  118 + GNU GPL, and only to the extent that such information is
  119 + necessary to install and execute a modified version of the
  120 + Combined Work produced by recombining or relinking the
  121 + Application with a modified version of the Linked Version. (If
  122 + you use option 4d0, the Installation Information must accompany
  123 + the Minimal Corresponding Source and Corresponding Application
  124 + Code. If you use option 4d1, you must provide the Installation
  125 + Information in the manner specified by section 6 of the GNU GPL
  126 + for conveying Corresponding Source.)
  127 +
  128 + 5. Combined Libraries.
  129 +
  130 + You may place library facilities that are a work based on the
  131 +Library side by side in a single library together with other library
  132 +facilities that are not Applications and are not covered by this
  133 +License, and convey such a combined library under terms of your
  134 +choice, if you do both of the following:
  135 +
  136 + a) Accompany the combined library with a copy of the same work based
  137 + on the Library, uncombined with any other library facilities,
  138 + conveyed under the terms of this License.
  139 +
  140 + b) Give prominent notice with the combined library that part of it
  141 + is a work based on the Library, and explaining where to find the
  142 + accompanying uncombined form of the same work.
  143 +
  144 + 6. Revised Versions of the GNU Lesser General Public License.
  145 +
  146 + The Free Software Foundation may publish revised and/or new versions
  147 +of the GNU Lesser General Public License from time to time. Such new
  148 +versions will be similar in spirit to the present version, but may
  149 +differ in detail to address new problems or concerns.
  150 +
  151 + Each version is given a distinguishing version number. If the
  152 +Library as you received it specifies that a certain numbered version
  153 +of the GNU Lesser General Public License "or any later version"
  154 +applies to it, you have the option of following the terms and
  155 +conditions either of that published version or of any later version
  156 +published by the Free Software Foundation. If the Library as you
  157 +received it does not specify a version number of the GNU Lesser
  158 +General Public License, you may choose any version of the GNU Lesser
  159 +General Public License ever published by the Free Software Foundation.
  160 +
  161 + If the Library as you received it specifies that a proxy can decide
  162 +whether future versions of the GNU Lesser General Public License shall
  163 +apply, that proxy's public statement of acceptance of any version is
  164 +permanent authorization for you to choose that version for the
  165 +Library.
3  README
... ... @@ -0,0 +1,3 @@
  1 +A very simple but flexible mass mailer.
  2 +
  3 +Try a demo: http://mailer.ksikes.net.
14 TODO
... ... @@ -0,0 +1,14 @@
  1 +IMPLEMENT:
  2 +
  3 +- keep a list of message / email address in order to avoid resending to the same email (unless force is checked)
  4 +- send a copy to ...
  5 +
  6 +MORE IDEAS:
  7 +
  8 +- have recipient lists which are SQL queries.
  9 +>> recipients creation should be part of another panel (more advanced one).
  10 +>> templates are connected to recipients (variables being used) so it might not be a good idea to disociate the two.
  11 +
  12 +- have an export data as CSV option.
  13 +- need to be able to save templates.
  14 +- need to keep rack of exactly which email was sent and to whom.
0  __init__.py
No changes.
0  app/__init__.py
No changes.
0  app/controllers/__init__.py
No changes.
47 app/controllers/handle_templates.py
... ... @@ -0,0 +1,47 @@
  1 +# Author: Alex Ksikes
  2 +
  3 +import web
  4 +import config
  5 +
  6 +from config import view
  7 +from web import form
  8 +
  9 +from app.models import templates
  10 +from app.helpers import utils
  11 +
  12 +sendmail_form = \
  13 + utils.Form(
  14 + form.Textbox('reply_to',
  15 + description='Reply to: '),
  16 + form.Textbox('to',
  17 + form.notnull,
  18 + description='To: ',
  19 + pre='<label class="help" for="to">Use the template variable email e.g. $email specified by the SQL query.</label>'),
  20 + form.Textbox('subject',
  21 + form.notnull,
  22 + description='Subject: ',
  23 + pre='<label class="help" for="to">Use any template variable e.g. $variable_name specified by the SQL query.</label>'),
  24 + form.Textarea('message',
  25 + form.notnull,
  26 + description='Message: ',
  27 + pre='<label class="help" for="to">Use any template variable e.g. $variable_name specified by the SQL query.</label>'),
  28 + form.Checkbox('send_copy',
  29 + description='',
  30 + post='<span>Send a copy to:</span> <span>%s</span>' % web.htmlquote(config.mail_bcc)),
  31 + form.Checkbox('force_resend',
  32 + description='',
  33 + post='<span>Force resend to previously emailed recipients.</span>'))
  34 +
  35 +class index:
  36 + def GET(self):
  37 + return view.base(view.index(view.sendmail_form(sendmail_form())))
  38 +
  39 +class send:
  40 + def POST(self):
  41 + f = sendmail_form()
  42 +
  43 + count = 0
  44 + if f.validates():
  45 + d = f.d; d.sql_query = web.input().sql_query
  46 + count = templates.send(d.to, d.subject, d.message, d.sql_query, d.reply_to)
  47 + return view.sendmail_form(f, success=f.valid, count=count)
24 app/controllers/preview_recipients.py
... ... @@ -0,0 +1,24 @@
  1 +# Author: Alex Ksikes
  2 +
  3 +import web
  4 +import config
  5 +
  6 +from config import view
  7 +
  8 +from app.helpers import paging
  9 +from app.models import recipients
  10 +
  11 +import re
  12 +
  13 +results_per_page = 15
  14 +
  15 +class browse:
  16 + def GET(self):
  17 + i = web.input(start=0, sql_query='')
  18 + start = int(i.start)
  19 +
  20 + results, count, columns = recipients.get(i.sql_query, offset=start, limit=results_per_page)
  21 + pager = web.storage(paging.get_paging(start, count,
  22 + results_per_page=results_per_page, window_size=1))
  23 +
  24 + return view.recipients_preview(results, columns, pager, i)
17 app/controllers/public.py
... ... @@ -0,0 +1,17 @@
  1 +# Author: Alex Ksikes
  2 +
  3 +import web
  4 +import mimetypes
  5 +
  6 +class public:
  7 + def GET(self):
  8 + public_dir = 'public'
  9 + try:
  10 + file_name = web.ctx.path.split('/')[-1]
  11 + web.header('Content-type', mime_type(file_name))
  12 + return open(public_dir + web.ctx.path, 'rb').read()
  13 + except IOError:
  14 + raise web.notfound()
  15 +
  16 +def mime_type(filename):
  17 + return mimetypes.guess_type(filename)[0] or 'application/octet-stream'
0  app/helpers/__init__.py
No changes.
64 app/helpers/misc.py
... ... @@ -0,0 +1,64 @@
  1 +# Author: Alex Ksikes
  2 +
  3 +import web
  4 +import config
  5 +
  6 +import re, urlparse, datetime, urllib, utils, string
  7 +
  8 +def format_date(d, f):
  9 + return d.strftime(f)
  10 +
  11 +def url_quote(url):
  12 + return web.urlquote(url)
  13 +
  14 +def html_quote(html):
  15 + return web.htmlquote(url)
  16 +
  17 +def url_encode(query, clean=True, doseq=True, **kw):
  18 + query = web.dictadd(query, kw)
  19 + if clean is True:
  20 + for q, v in query.items():
  21 + if not v:
  22 + del query[q]
  23 + return urllib.urlencode(query, doseq)
  24 +
  25 +def cut_length(s, max=40):
  26 + if len(s) > max:
  27 + s = s[0:max] + '...'
  28 + return s
  29 +
  30 +def get_nice_url(url):
  31 + host, path = urlparse.urlparse(url)[1:3]
  32 + if path == '/':
  33 + path = ''
  34 + return cut_length(host+path)
  35 +
  36 +def capitalize_first(str):
  37 + if not str:
  38 + str = ''
  39 + return ' '.join(map(string.capitalize, str.lower().split()))
  40 +
  41 +def text2html(s):
  42 + s = replace_breaks(s)
  43 + s = replace_indents(s)
  44 + return replace_links(s)
  45 +
  46 +def replace_breaks(s):
  47 + return re.sub('\n', '<br />', s)
  48 +
  49 +def replace_indents(s):
  50 + s = re.sub('\t', 4*' ', s)
  51 + return re.sub('\s{2}', '&nbsp;'*2, s)
  52 +
  53 +def replace_links(s):
  54 + return re.sub('(http://[^\s]+)', r'<a rel="nofollow" href="\1">' + get_nice_url(r'\1') + '</a>', s, re.I)
  55 +
  56 +# we may need to get months ago as well
  57 +def how_long(d):
  58 + return web.datestr(d, datetime.datetime.now())
  59 +
  60 +def split(pattern, str):
  61 + return re.split(pattern, str)
  62 +
  63 +def get_site_config(attr):
  64 + return getattr(config, attr)
72 app/helpers/paging.py
... ... @@ -0,0 +1,72 @@
  1 +# Author : Alex Ksikes
  2 +
  3 +import urllib
  4 +
  5 +def get_paging(start, max_results, query=False, results_per_page=15, window_size=15):
  6 + c_page = start / results_per_page + 1
  7 + if not start:
  8 + c_page = 1
  9 + nb_pages = max_results / results_per_page
  10 + if max_results % results_per_page != 0:
  11 + nb_pages += 1
  12 +
  13 + left_a = right_a = ''
  14 + if c_page > 1:
  15 + left_a = (c_page - 2) * results_per_page
  16 + if c_page < nb_pages:
  17 + right_a = start + results_per_page
  18 +
  19 + leftmost_a = rightmost_a = ''
  20 + if (c_page - 3) >= 0:
  21 + leftmost_a = 0
  22 + if (c_page + 1) < nb_pages:
  23 + rightmost_a = (nb_pages -1) * results_per_page
  24 +
  25 + left = c_page - window_size / 2
  26 + if left < 1:
  27 + left = 1
  28 + right = left + window_size - 1
  29 + if right > nb_pages:
  30 + right = nb_pages
  31 +
  32 + pages = []
  33 + for i in range(left, right + 1):
  34 + pages.append({
  35 + 'number' : i,
  36 + 'start' : (i - 1) * results_per_page
  37 + })
  38 +
  39 + return {
  40 + 'start' : start,
  41 + 'max_results' : max_results,
  42 + 'c_page' : c_page,
  43 + 'nb_pages' : nb_pages,
  44 + 'pages' : pages,
  45 + 'leftmost_a' : leftmost_a,
  46 + 'left_a' : left_a,
  47 + 'right_a' : right_a,
  48 + 'rightmost_a' : rightmost_a,
  49 + 'query_enc' : query and urllib.quote(query) or ''
  50 + }
  51 +
  52 +def get_paging_results(start, max_results, id, results, results_per_page):
  53 + index = 0
  54 + results = list(results)
  55 + for i, r in enumerate(results):
  56 + if r.id == id: index = i
  57 + pager = dict(left_start=start, right_start=start, max_results=max_results,
  58 + number=start + index, left=False, middle=results[index], right=False)
  59 +
  60 + if index > 0 or start > 0:
  61 + pager['left'] = results[index-1]
  62 + if index < len(results) - 1:
  63 + pager['right'] = results[index+1]
  64 + if index == results_per_page - 1:
  65 + pager['right_start'] = start + results_per_page
  66 + if index == 0 and start > 0:
  67 + pager['left_start'] = start - results_per_page
  68 + if start != 0:
  69 + pager['number'] = start + index -1
  70 +
  71 + return pager
  72 +
57 app/helpers/utils.py
... ... @@ -0,0 +1,57 @@
  1 +# Author: Alex Ksikes
  2 +
  3 +import web
  4 +import config
  5 +
  6 +import md5, time, types
  7 +
  8 +def dict_remove(d, *keys):
  9 + for k in keys:
  10 + if d.has_key(k):
  11 + del d[k]
  12 +
  13 +def make_unique_md5():
  14 + return md5.md5(time.ctime() + config.encryption_key).hexdigest()
  15 +
  16 +def get_all_functions(module):
  17 + functions = {}
  18 + for f in [module.__dict__.get(a) for a in dir(module)
  19 + if isinstance(module.__dict__.get(a), types.FunctionType)]:
  20 + functions[f.__name__] = f
  21 + return functions
  22 +
  23 +def sqlands(left, lst):
  24 + """Similar to webpy sqlors but for ands"""
  25 + if isinstance(lst, web.utils.iters):
  26 + lst = list(lst)
  27 + ln = len(lst)
  28 + if ln == 0:
  29 + return web.SQLQuery("1!=2")
  30 + if ln == 1:
  31 + lst = lst[0]
  32 + if isinstance(lst, web.utils.iters):
  33 + return web.SQLQuery(['('] +
  34 + sum([[left, web.sqlparam(x), ' AND '] for x in lst], []) +
  35 + ['1!=2)']
  36 + )
  37 + else:
  38 + return left + web.sqlparam(lst)
  39 +
  40 +def get_ip():
  41 + return web.ctx.get('ip', '000.000.000.000')
  42 +
  43 +class Form(web.form.Form):
  44 + def render_css(self):
  45 + out = []
  46 + out.append(self.rendernote(self.note))
  47 + for i in self.inputs:
  48 + if i.note:
  49 + out.append('<div class="wrong" id="box_%s">' % i.id)
  50 + else:
  51 + out.append('<div id="box_%s">' % i.id)
  52 + out.append('<label for="%s">%s</label>' % (i.id, web.websafe(i.description)))
  53 + out.append(i.pre)
  54 + out.append(i.render())
  55 + out.append(i.post)
  56 + out.append('</div>\n')
  57 + return ''.join(out)
0  app/models/__init__.py
No changes.
28 app/models/recipients.py
... ... @@ -0,0 +1,28 @@
  1 +# Author: Alex Ksikes
  2 +
  3 +import web
  4 +import config
  5 +
  6 +import MySQLdb, re
  7 +
  8 +# we are forced to use MySQLdb in order to keep the columns order
  9 +def __get_cursor():
  10 + param = config.db_records_settings
  11 + return MySQLdb.connect(user=param.user, passwd=param.passwd, db=param.db, use_unicode=True).cursor()
  12 +
  13 +def get(sql_query, offset, limit):
  14 + c = __get_cursor()
  15 +
  16 + c.execute(sql_query + ' limit %s offset %s' % (limit, offset))
  17 + columns = [x[0] for x in c.description]
  18 + results = c.fetchall()
  19 +
  20 + query_count = re.sub('select .*? from', 'select count(*) from', sql_query, re.I)
  21 + c.execute(query_count)
  22 + count = c.fetchall()[0][0]
  23 +
  24 + return (results, count, columns)
  25 +
  26 +def get_all(sql_query):
  27 + db = web.database(**config.db_records_settings)
  28 + return db.query(sql_query)
34 app/models/templates.py
... ... @@ -0,0 +1,34 @@
  1 +# Author: Alex Ksikes
  2 +
  3 +import web
  4 +import config
  5 +from config import db
  6 +
  7 +from app.models import recipients
  8 +
  9 +import re
  10 +
  11 +def send(to_tpl, subject_tpl, message_tpl, sql_query='', reply_to=''):
  12 + headers = dict(Bcc=config.mail_bcc)
  13 + if reply_to: headers['Reply-To'] = reply_to
  14 +
  15 + count = 1
  16 + if not sql_query:
  17 + web.sendmail(config.mail_sender, to_tpl, subject_tpl, message_tpl, headers=headers)
  18 + else:
  19 + recipients_ = recipients.get_all(sql_query)
  20 + count = len(recipients_)
  21 + for recipient in recipients_:
  22 + to = instantiate_from_template(to_tpl, recipient)
  23 + subject = instantiate_from_template(subject_tpl, recipient)
  24 + message = instantiate_from_template(message_tpl, recipient)
  25 +
  26 + web.sendmail(config.mail_sender, to, subject, message, headers=headers)
  27 + return count
  28 +
  29 +def instantiate_from_template(tpl, recipient):
  30 + # we can't do this! web.template.Template('$def with (**recipient)\n %s' % tpl)(**recipient)
  31 + tpl = re.sub('\$(\w+)', '$recipient.\\1', tpl, re.S)
  32 + tpl = '$def with (recipient)\n%s' % tpl
  33 +
  34 + return web.template.Template(tpl)(recipient).__body__[:-1]
34 app/views/base.html
... ... @@ -0,0 +1,34 @@
  1 +$def with (content)
  2 +
  3 +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
  4 +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
  5 +
  6 +<head>
  7 +<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
  8 +<meta http-equiv="Content-Language" content="en-us"/>
  9 +
  10 +<title>Mailer</title>
  11 + <link href="/css/defaults.css" rel="stylesheet" type="text/css"/>
  12 + <link rel="icon" type="image/png" href="/img/favicon.ico"/>
  13 + <script type="text/javascript" src="/js/jquery-1.3.2.js"></script>
  14 + <script type="text/javascript" src="/js/defaults.js"></script>
  15 +</head>
  16 +
  17 +<body>
  18 +
  19 +
  20 +$#<div class="topmenu">
  21 +$# <ul>
  22 +$# <li><a href="/">Send Templates</a></li>
  23 +$# <li>Settings</li>
  24 +$# </ul>
  25 +$#</div>
  26 +
  27 +<div class="content">
  28 +$:content
  29 +</div>
  30 +
  31 +<div class="footer">
  32 +</div>
  33 +
  34 +</body>
25 app/views/index.html
... ... @@ -0,0 +1,25 @@
  1 +$def with (form)
  2 +
  3 +<form method="post" action="/send">
  4 +
  5 +<div id="template">
  6 + <h2>Write your template:</h2>
  7 +
  8 + <div id="sendmail_form">
  9 + $:form
  10 + </div>
  11 +</div>
  12 +
  13 +<div id="recipients">
  14 + <h2>Choose your recipients:</h2>
  15 +
  16 + <label for="sql_query">Select a list of recipients from a SQL query:</label>
  17 + <label class="help" for="sql_query">Make sure user only has select rights! Your email could be sent to all these recipients so watch out. Does not work with limit and offset.</label>
  18 + <textarea name="sql_query" id="sql_query"></textarea>
  19 + <button name="submit" type="button" onclick="preview_recipients()">Preview Recipients</button>
  20 +
  21 + <div id="recipients_preview">
  22 + </div>
  23 +</div>
  24 +
  25 +</form>
50 app/views/recipients_preview.html
... ... @@ -0,0 +1,50 @@
  1 +$def with (results, columns, pager, inputs)
  2 +
  3 +$def show_paging():
  4 + $if pager.leftmost_a or pager.leftmost_a == 0:
  5 + <a href="javascript:ajax_load('/recipients?$url_encode(inputs, start=pager.leftmost_a)')"><< First</a>
  6 + $if pager.left_a or pager.left_a == 0:
  7 + <a href="javascript:ajax_load('/recipients?$url_encode(inputs, start=pager.left_a)')">< Prev</a>
  8 + $(pager.start + 1) -
  9 + $if pager.right_a:
  10 + $pager.right_a
  11 + $else:
  12 + $pager.max_results
  13 + of $pager.max_results
  14 + $if pager.right_a:
  15 + <a href="javascript:ajax_load('/recipients?$url_encode(inputs, start=pager.right_a)')">Next ></a>
  16 + $if pager.rightmost_a:
  17 + <a href="javascript:ajax_load('/recipients?$url_encode(inputs, start=pager.rightmost_a)')">Last >></a>
  18 +
  19 +$def show_table_header():
  20 + <tr>
  21 + $for column in columns:
  22 + <th>\$$column</th>
  23 + </tr>
  24 +
  25 +<p class="label">Variables you may now use in your template:</p>
  26 +<label class="help">You can use any of these variables in your template.</label>
  27 +<div class="template_variables">
  28 +$for column in columns:
  29 + <span>\$$column</span>
  30 +</div>
  31 +
  32 +<p class="preview_title label">Your email will be sent to everyone on this list:</p>
  33 +<div class="paging">
  34 + $:show_paging()
  35 +</div>
  36 +
  37 +<div class="recipients_table">
  38 +<table>
  39 +$:show_table_header()
  40 +$for row in results:
  41 + <tr>
  42 + $#$for value in row.values():
  43 + $for value in row:
  44 + <td>
  45 + $if value: $value
  46 + $else: -
  47 + </td>
  48 + </tr>
  49 +</table>
  50 +</div>
16 app/views/sendmail_form.html
... ... @@ -0,0 +1,16 @@
  1 +$def with (form, success=False, count=0)
  2 +
  3 +$if success:
  4 + <div class="success">$count email(s) sent.</div>
  5 +
  6 +<label for="from">From: </label>
  7 +<div class="fixed_input">$get_site_config('mail_sender')</div>
  8 +$:form.render_css()
  9 +<button name="submit" type="submit" onclick="send_mails()">Send Emails</button>
  10 +
  11 +$#<span id="test_sql_query"><a class="animated" href="javascript:test_sql_query()">Test SQL query &raquo;</a></span>
  12 +$#<form method="post" action="/send">
  13 +$# $:form.render_css()
  14 +$# <button name="submit" type="button" onclick="sendmails()">Send Emails</button>
  15 +$# $#<button name="submit" type="submit">Send Emails</button>
  16 +$#</form>
21 application.py
... ... @@ -0,0 +1,21 @@
  1 +#!/usr/bin/env python
  2 +# Author: Alex Ksikes
  3 +
  4 +import web
  5 +import config
  6 +import app.controllers
  7 +
  8 +urls = (
  9 + '/', 'app.controllers.handle_templates.index',
  10 + '/send', 'app.controllers.handle_templates.send',
  11 + '/recipients', 'app.controllers.preview_recipients.browse',
  12 +
  13 + '/(?:img|js|css)/.*', 'app.controllers.public.public',
  14 +)
  15 +
  16 +app = web.application(urls, globals())
  17 +if web.config.email_errors:
  18 + app.internalerror = web.emailerrors(web.config.email_errors, web.webapi._InternalError)
  19 +
  20 +if __name__ == "__main__":
  21 + app.run()
41 config_example.py
... ... @@ -0,0 +1,41 @@
  1 +# Author: Alex Ksikes
  2 +
  3 +import web
  4 +
  5 +from app.helpers import misc
  6 +from app.helpers import utils
  7 +
  8 +# connect to the mailer database.
  9 +db = web.database(dbn='mysql', db='mailer', user='user', passwd='password')
  10 +
  11 +# in development debug error messages and reloader.
  12 +web.config.debug = False
  13 +
  14 +# in develpment template caching is set to false.
  15 +cache = True
  16 +
  17 +# global used template functions.
  18 +globals = utils.get_all_functions(misc)
  19 +
  20 +# the domain where the site is hosted.
  21 +site_domain = 'www.your_domain.com'
  22 +
  23 +# set global base template.
  24 +view = web.template.render('app/views', cache=cache, globals=globals)
  25 +
  26 +# used as a salt.
  27 +encryption_key = 'a random string'
  28 +
  29 +# in production the internal errors could be emailed to us.
  30 +web.config.email_errors = ''
  31 +
  32 +# email of the sender.
  33 +# IMPORTANT: make sure SPF and reverse DNS are setup to avoid mails being marked as spam.
  34 +mail_sender = 'Mailer sender name <noreply@example.com>'
  35 +
  36 +# used for the send a copy feature.
  37 +mail_bcc = 'Send a copy to name <example@example.com>'
  38 +
  39 +# db setting used to browse through your databases.
  40 +# IMPORTANT: make sure user only has select rights.
  41 +db_records_settings = web.storage(dbn='mysql', db='mlss', user="user", passwd="password")
225 public/css/defaults.css
... ... @@ -0,0 +1,225 @@
  1 +/* defaults */
  2 +body, h3, ul, li, form {
  3 + margin: 0px;
  4 + padding: 0px;
  5 +}
  6 +
  7 +body, form {
  8 + font-family: arial, sans-serif;
  9 + font-size: 13px;
  10 + line-height: 19px;
  11 +}
  12 +
  13 +a {
  14 + color : #0000CC;
  15 +}
  16 +
  17 +a img {
  18 + border: 0pt;
  19 +}
  20 +
  21 +strong.wrong {
  22 + color: red;
  23 + float: right;
  24 +}
  25 +
  26 +div.wrong input, div.wrong textarea {
  27 + background: #FFDFDF;
  28 +}
  29 +
  30 +.success {
  31 + background-color: #FFD37F;
  32 + text-align: center;
  33 + padding: 5px;
  34 + -moz-border-radius: 5px;
  35 + font-weight: bold;
  36 +/* margin-bottom: 10px;*/
  37 + position: absolute;
  38 + top: 5px;
  39 + right: 20px;
  40 + width: 235px;
  41 +}
  42 +
  43 +a.animated {
  44 + text-decoration: none;
  45 +}
  46 +
  47 +a.animated:hover {
  48 + text-decoration: underline;
  49 +}
  50 +/* defaults */
  51 +
  52 +/* forms defaults */
  53 +form label, .label {
  54 + color: #0066CC;
  55 + display: block;
  56 + margin: 3px 0px;
  57 + cursor: pointer;
  58 + font-weight: bold;
  59 +}
  60 +
  61 +form label.help {
  62 + color: #7F7F7F;
  63 + font-weight: normal;
  64 +}
  65 +
  66 +form input, form textarea, form select {
  67 + width: 100%;
  68 +}
  69 +
  70 +#sendmail_form div {
  71 + margin-bottom: 15px;
  72 +}
  73 +
  74 +textarea {
  75 + width: 100%;
  76 + height: 350px;
  77 +}
  78 +/* forms defaults */
  79 +
  80 +/* html table */
  81 +table {
  82 + /* IE need to apply at the table level */
  83 + empty-cells : show;
  84 + border-collapse : collapse;
  85 + width: 100%;
  86 + /*table-layout: fixed;*/
  87 +}
  88 +
  89 +td, th {
  90 +/* width: 100%; */
  91 + font-size : 90%;
  92 + white-space : nowrap;
  93 +/* padding-right: 10px;
  94 + max-width: 185px;*/
  95 + overflow: hidden;
  96 +}
  97 +
  98 +th {
  99 + font-weight: normal;
  100 + background-color: #EFEFEF;
  101 + text-align: left;
  102 +}
  103 +/* html table */
  104 +
  105 +/* sql query */
  106 +#sql_query {
  107 + height: 50px;
  108 +}
  109 +
  110 +#test_sql_query {
  111 +/* position: absolute;
  112 + right: 0px;
  113 + top: 5px;*/
  114 + float: right;
  115 + font-weight: bold;
  116 + font-size: 90%;
  117 +}
  118 +/* sql query */
  119 +
  120 +.content {
  121 + padding: 15px;
  122 + overflow: auto;
  123 +}
  124 +
  125 +.fixed_input {
  126 + border: 2px solid gray;
  127 + padding: 3px;
  128 + margin-bottom: 15px;
  129 +}
  130 +
  131 +#recipients .template_variables {
  132 + border: 2px dotted gray;
  133 + padding: 5px;
  134 + margin-bottom: 15px;
  135 + -moz-border-radius: 5px;
  136 + font-weight: bold;
  137 +}
  138 +
  139 +#template {
  140 + float: left;
  141 + /*width: 400px;*/
  142 + width: 30%;
  143 + margin-right: 2%;
  144 +}
  145 +
  146 +#recipients {
  147 + /*position: absolute;
  148 + margin-left: 420px;
  149 + padding: 0px 15px;*/
  150 + padding-left: 2%;
  151 + border-left: 2px dotted grey;
  152 + width: 65%;
  153 + float: left;
  154 +}
  155 +
  156 +#recipients .paging {
  157 + float: right;
  158 + padding: 2px;
  159 + font-weight: bold;
  160 + font-size: 90%;
  161 +}
  162 +
  163 +#recipients table td, #recipients table th {
  164 + border-bottom : 1px dotted;
  165 + padding-top: 3px;
  166 + padding-right: 10px;
  167 +}
  168 +
  169 +#recipients .preview_title {
  170 + float: left;
  171 + margin: 0px;
  172 +}
  173 +
  174 +#recipients .recipients_table {
  175 + clear: both;
  176 + overflow: scroll;
  177 +}
  178 +
  179 +.sending {
  180 + width: 90px;
  181 + padding: 3px;
  182 + background: url(/img/loading.gif) no-repeat right;
  183 +}
  184 +
  185 +/* checkboxes */
  186 +form #send_copy, form #force_resend {
  187 + width: 15px;
  188 + margin: 0px 3px 0px 0px;
  189 +}
  190 +/* checkboxes */
  191 +
  192 +#recipients button {
  193 + margin: 15px 0px;
  194 +}
  195 +
  196 +/* top menu */
  197 +.topmenu {
  198 + border-bottom: 1px solid #C0C0C0;
  199 + height: 25px;
  200 + line-height: 25px;
  201 + margin-bottom: 15px;
  202 + position: relative;
  203 + z-index: 2;
  204 +}
  205 +
  206 +.topmenu ul {
  207 + font-size: 12px;
  208 + margin: -2px 3px 0;
  209 + position: absolute;
  210 + right: 0;
  211 + top: 3px;
  212 +}
  213 +
  214 +.topmenu li {
  215 + border-right: 1px solid #C0C0C0;
  216 + display: inline;
  217 + padding:0px 10px;
  218 +}
  219 +
  220 +.topmenu li.last {
  221 + ul.extlinks li.last {defaults.css (line 133)
  222 + border:medium none;
  223 + padding-right:0;
  224 +}
  225 +/* top menu */
BIN  public/img/email_go.png
BIN  public/img/favicon.ico
Binary file not shown
BIN  public/img/loading.gif
25 public/js/defaults.js
... ... @@ -0,0 +1,25 @@
  1 +function preview_recipients() {
  2 + var sql_query = $('#sql_query')[0].value;
  3 + $("#recipients_preview").load("/recipients?sql_query="+escape(sql_query));
  4 +}
  5 +
  6 +function ajax_load(url) {
  7 + $("#recipients_preview").load(url);
  8 +}
  9 +
  10 +function send_mails() {
  11 + $('#template button').replaceWith('<div class="sending">Sending ...</div>');
  12 + var data = new Object();
  13 + $('#sendmail_form input, #sendmail_form textarea, #recipients textarea').each(
  14 + function() {
  15 + /* on firefox javascript considers no value for a checkbox as a checked checkbox */
  16 + if (this.type == 'checkbox') {
  17 + data[this.name] = this.checked || '';
  18 + } else {
  19 + data[this.name] = this.value;
  20 + }
  21 + }
  22 + );
  23 + $('#sendmail_form').load('/send', data);
  24 + return false;
  25 +}
4,376 public/js/jquery-1.3.2.js
4,376 additions, 0 deletions not shown
0  schema.sql
No changes.

0 comments on commit aa1719b

Please sign in to comment.
Something went wrong with that request. Please try again.