Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Initial commit.

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