@@ -0,0 +1,40 @@ | |||
{ | |||
"base_template": "lib/website/templates/base.html", | |||
"framework_version": "3.9.0", | |||
"modules": { | |||
"Calendar": { | |||
"color": "#2980b9", | |||
"icon": "icon-calendar", | |||
"label": "Calendar", | |||
"link": "Calendar/Event", | |||
"type": "view" | |||
}, | |||
"Finder": { | |||
"color": "#14C7DE", | |||
"icon": "icon-folder-open", | |||
"label": "Finder", | |||
"link": "finder", | |||
"type": "page" | |||
}, | |||
"Messages": { | |||
"color": "#9b59b6", | |||
"icon": "icon-comments", | |||
"label": "Messages", | |||
"link": "messages", | |||
"type": "page" | |||
}, | |||
"To Do": { | |||
"color": "#f1c40f", | |||
"icon": "icon-check", | |||
"label": "To Do", | |||
"link": "todo", | |||
"type": "page" | |||
}, | |||
"Website": { | |||
"color": "#16a085", | |||
"icon": "icon-globe", | |||
"link": "website-home", | |||
"type": "module" | |||
} | |||
} | |||
} |
@@ -18,4 +18,5 @@ slugify | |||
termcolor | |||
werkzeug | |||
semantic_version | |||
lxml | |||
inlinestyler |
@@ -220,6 +220,13 @@ def set_user(username): | |||
def get_request_header(key, default=None): | |||
return request.headers.get(key, default) | |||
def sendmail(recipients=[], sender="", subject="No Subject", message="No Message", as_markdown=False): | |||
import webnotes.utils.email_lib | |||
if as_markdown: | |||
webnotes.utils.email_lib.sendmail_md(recipients, sender=sender, subject=subject, msg=message) | |||
else: | |||
webnotes.utils.email_lib.sendmail(recipients, sender=sender, subject=subject, msg=message) | |||
logger = None | |||
whitelisted = [] | |||
guest_methods = [] | |||
@@ -3,6 +3,13 @@ | |||
from __future__ import unicode_literals | |||
import webnotes | |||
import json | |||
import urllib | |||
from email.utils import formataddr | |||
from webnotes.webutils import is_signup_enabled | |||
from webnotes.utils import get_url, cstr | |||
from webnotes.utils.email_lib.email_body import get_email | |||
from webnotes.utils.email_lib.smtp import send | |||
class DocType(): | |||
def __init__(self, doc, doclist=[]): | |||
@@ -36,13 +43,11 @@ def make(doctype=None, name=None, content=None, subject=None, sent_or_received = | |||
# since we are using fullname and email, | |||
# if the fullname has any incompatible characters,formataddr can deal with it | |||
try: | |||
import json | |||
sender = json.loads(sender) | |||
except ValueError: | |||
pass | |||
if isinstance(sender, (tuple, list)) and len(sender)==2: | |||
from email.utils import formataddr | |||
sender = formataddr(sender) | |||
comm = webnotes.new_bean('Communication') | |||
@@ -76,7 +81,6 @@ def get_customer_supplier(args=None): | |||
""" | |||
Get Customer/Supplier, given a contact, if a unique match exists | |||
""" | |||
import webnotes | |||
if not args: args = webnotes.local.form_dict | |||
if not args.get('contact'): | |||
raise Exception, "Please specify a contact to fetch Customer/Supplier" | |||
@@ -92,7 +96,6 @@ def get_customer_supplier(args=None): | |||
return {} | |||
def send_comm_email(d, name, sent_via=None, print_html=None, attachments='[]', send_me_a_copy=False): | |||
from json import loads | |||
footer = None | |||
if sent_via: | |||
@@ -105,7 +108,6 @@ def send_comm_email(d, name, sent_via=None, print_html=None, attachments='[]', s | |||
footer = set_portal_link(sent_via, d) | |||
from webnotes.utils.email_lib.smtp import get_email | |||
mail = get_email(d.recipients, sender=d.sender, subject=d.subject, | |||
msg=d.content, footer=footer) | |||
@@ -115,20 +117,17 @@ def send_comm_email(d, name, sent_via=None, print_html=None, attachments='[]', s | |||
if print_html: | |||
mail.add_attachment(name.replace(' ','').replace('/','-') + '.html', print_html) | |||
for a in loads(attachments): | |||
for a in json.loads(attachments): | |||
try: | |||
mail.attach_file(a) | |||
except IOError, e: | |||
webnotes.msgprint("""Unable to find attachment %s. Please resend without attaching this file.""" % a, | |||
raise_exception=True) | |||
mail.send() | |||
send(mail) | |||
def set_portal_link(sent_via, comm): | |||
"""set portal link in footer""" | |||
from webnotes.webutils import is_signup_enabled | |||
from webnotes.utils import get_url, cstr | |||
import urllib | |||
footer = None | |||
@@ -143,29 +142,3 @@ def set_portal_link(sent_via, comm): | |||
<a href="%s" target="_blank">View this on our website</a>""" % url | |||
return footer | |||
def get_user(doctype, txt, searchfield, start, page_len, filters): | |||
from erpnext.controllers.queries import get_match_cond | |||
return webnotes.conn.sql("""select name, concat_ws(' ', first_name, middle_name, last_name) | |||
from `tabProfile` | |||
where ifnull(enabled, 0)=1 | |||
and docstatus < 2 | |||
and (%(key)s like "%(txt)s" | |||
or concat_ws(' ', first_name, middle_name, last_name) like "%(txt)s") | |||
%(mcond)s | |||
limit %(start)s, %(page_len)s """ % {'key': searchfield, | |||
'txt': "%%%s%%" % txt, 'mcond':get_match_cond(doctype, searchfield), | |||
'start': start, 'page_len': page_len}) | |||
def get_lead(doctype, txt, searchfield, start, page_len, filters): | |||
from erpnext.controllers.queries import get_match_cond | |||
return webnotes.conn.sql(""" select name, lead_name from `tabLead` | |||
where docstatus < 2 | |||
and (%(key)s like "%(txt)s" | |||
or lead_name like "%(txt)s" | |||
or company_name like "%(txt)s") | |||
%(mcond)s | |||
order by lead_name asc | |||
limit %(start)s, %(page_len)s """ % {'key': searchfield,'txt': "%%%s%%" % txt, | |||
'mcond':get_match_cond(doctype, searchfield), 'start': start, | |||
'page_len': page_len}) |
@@ -7,6 +7,8 @@ from webnotes import conf | |||
import webnotes.utils | |||
from webnotes.modules import get_doc_path | |||
standard_format = "templates/print_formats/standard.html" | |||
class DocType: | |||
def __init__(self, d, dl): | |||
self.doc, self.doclist = d,dl | |||
@@ -39,8 +41,9 @@ class DocType: | |||
webnotes.clear_cache(doctype=self.doc.doc_type) | |||
def get_args(): | |||
if not webnotes.form_dict.doctype or not webnotes.form_dict.name \ | |||
or not webnotes.form_dict.format: | |||
if not webnotes.form_dict.format: | |||
webnotes.form_dict.format = standard_format | |||
if not webnotes.form_dict.doctype or not webnotes.form_dict.name: | |||
return { | |||
"body": """<h1>Error</h1> | |||
<p>Parameters doctype, name and format required</p> | |||
@@ -61,12 +64,12 @@ def get_args(): | |||
"comment": webnotes.session.user | |||
} | |||
def get_html(doc, doclist): | |||
def get_html(doc, doclist, print_format=None): | |||
from jinja2 import Environment | |||
from webnotes.core.doctype.print_format.print_format import get_print_format | |||
template = Environment().from_string(get_print_format(doc.doctype, | |||
webnotes.form_dict.format)) | |||
print_format or webnotes.form_dict.format)) | |||
doctype = webnotes.get_doctype(doc.doctype) | |||
args = { | |||
@@ -79,15 +82,18 @@ def get_html(doc, doclist): | |||
html = template.render(args) | |||
return html | |||
def get_print_format(doctype, format): | |||
def get_print_format(doctype, format_name): | |||
if format_name==standard_format: | |||
return format_name | |||
# server, find template | |||
path = os.path.join(get_doc_path(webnotes.conn.get_value("DocType", doctype, "module"), | |||
"Print Format", format), format + ".html") | |||
"Print Format", format_name), format_name + ".html") | |||
if os.path.exists(path): | |||
with open(path, "r") as pffile: | |||
return pffile.read() | |||
else: | |||
html = webnotes.conn.get_value("Print Format", format, "html") | |||
html = webnotes.conn.get_value("Print Format", format_name, "html") | |||
if html: | |||
return html | |||
else: | |||
@@ -34,7 +34,7 @@ cur_frm.cscript.user_image = function(doc) { | |||
} | |||
cur_frm.cscript.refresh = function(doc) { | |||
if(!doc.__unsaved && doc.language !== wn.boot.profile.language) { | |||
if(!doc.__unsaved && wn.languages && doc.language !== wn.boot.profile.language) { | |||
msgprint("Refreshing..."); | |||
window.location.reload(); | |||
} | |||
@@ -2,7 +2,7 @@ | |||
# MIT License. See license.txt | |||
from __future__ import unicode_literals | |||
import webnotes, json | |||
import webnotes, json, os | |||
from webnotes.utils import cint, now, cstr | |||
from webnotes import throw, msgprint, _ | |||
from webnotes.auth import _update_password | |||
@@ -127,73 +127,21 @@ class DocType: | |||
(self.doc.first_name and " " or '') + (self.doc.last_name or '') | |||
def password_reset_mail(self, link): | |||
"""reset password""" | |||
txt = """ | |||
## %(title)s | |||
#### Password Reset | |||
Dear %(first_name)s, | |||
Please click on the following link to update your new password: | |||
<a href="%(link)s">%(link)s</a> | |||
Thank you,<br> | |||
%(user_fullname)s | |||
""" | |||
self.send_login_mail("Password Reset", | |||
txt, {"link": link}) | |||
self.send_login_mail("Password Reset", "templates/emails/password_reset.html", {"link": link}) | |||
def password_update_mail(self, password): | |||
txt = """ | |||
## %(title)s | |||
#### Password Update Notification | |||
Dear %(first_name)s, | |||
Your password has been updated. Here is your new password: %(new_password)s | |||
Thank you,<br> | |||
%(user_fullname)s | |||
""" | |||
self.send_login_mail("Password Update", | |||
txt, {"new_password": password}) | |||
self.send_login_mail("Password Update", "templates/emails/password_update.html", {"new_password": password}) | |||
def send_welcome_mail(self): | |||
"""send welcome mail to user with password and login url""" | |||
from webnotes.utils import random_string, get_url | |||
self.doc.reset_password_key = random_string(32) | |||
link = get_url("/update-password?key=" + self.doc.reset_password_key) | |||
txt = """ | |||
## %(title)s | |||
Dear %(first_name)s, | |||
A new account has been created for you. | |||
Your login id is: %(user)s | |||
To complete your registration, please click on the link below: | |||
<a href="%(link)s">%(link)s</a> | |||
Thank you,<br> | |||
%(user_fullname)s | |||
""" | |||
self.send_login_mail("New Account", txt, | |||
{ "link": link }) | |||
def send_login_mail(self, subject, txt, add_args): | |||
self.send_login_mail("Verify Your Account", "templates/emails/new_user.html", {"link": link}) | |||
def send_login_mail(self, subject, template, add_args): | |||
"""send mail with login details""" | |||
import os | |||
from webnotes.utils.email_lib import sendmail_md | |||
from webnotes.profile import get_user_fullname | |||
from webnotes.utils import get_url | |||
@@ -216,7 +164,8 @@ Thank you,<br> | |||
sender = webnotes.session.user not in ("Administrator", "Guest") and webnotes.session.user or None | |||
sendmail_md(recipients=self.doc.email, sender=sender, subject=subject, msg=txt % args) | |||
webnotes.sendmail(recipients=self.doc.email, sender=sender, subject=subject, | |||
message=webnotes.get_template(template).render(args)) | |||
def a_system_manager_should_exist(self): | |||
if not self.get_other_system_managers(): | |||
@@ -89,23 +89,13 @@ def delete(arg=None): | |||
def notify(arg=None): | |||
from webnotes.utils import cstr, get_fullname, get_url | |||
fn = get_fullname(webnotes.user.name) or webnotes.user.name | |||
url = get_url() | |||
message = '''You have a message from <b>%s</b>: | |||
%s | |||
To answer, please login to your erpnext account at \ | |||
<a href=\"%s\" target='_blank'>%s</a> | |||
''' % (fn, arg['txt'], url, url) | |||
sender = webnotes.conn.get_value("Profile", webnotes.user.name, "email") \ | |||
or webnotes.user.name | |||
recipient = [webnotes.conn.get_value("Profile", arg["contact"], "email") \ | |||
or arg["contact"]] | |||
from webnotes.utils.email_lib import sendmail | |||
sendmail(recipient, sender, message, arg.get("subject") or "You have a message from %s" % (fn,)) | |||
webnotes.sendmail(\ | |||
recipients=[webnotes.conn.get_value("Profile", arg["contact"], "email") or arg["contact"]], | |||
sender= webnotes.conn.get_value("Profile", webnotes.session.user, "email"), | |||
subject="New Message from " + get_fullname(webnotes.user.name), | |||
message=webnotes.get_template("templates/emails/new_message.html").render({ | |||
"from": get_fullname(webnotes.user.name), | |||
"message": arg['txt'], | |||
"link": get_url() | |||
}) | |||
) |
@@ -1,3 +1,5 @@ | |||
execute:import inlinestyler # new requirement | |||
execute:webnotes.reload_doc('core', 'doctype', 'doctype', force=True) #2014-01-24 | |||
execute:webnotes.reload_doc('core', 'doctype', 'docfield', force=True) #2013-13-26 | |||
execute:webnotes.reload_doc('core', 'doctype', 'docperm') #2013-13-26 | |||
@@ -1,10 +1,18 @@ | |||
/*! | |||
* Bootstrap v3.0.3 (http://getbootstrap.com) | |||
* Bootstrap v3.1.0 (http://getbootstrap.com) | |||
* Copyright 2011-2014 Twitter, Inc. | |||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) | |||
*/ | |||
/*! normalize.css v2.1.3 | MIT License | git.io/normalize */ | |||
/*! normalize.css v3.0.0 | MIT License | git.io/normalize */ | |||
html { | |||
font-family: sans-serif; | |||
-webkit-text-size-adjust: 100%; | |||
-ms-text-size-adjust: 100%; | |||
} | |||
body { | |||
margin: 0; | |||
} | |||
article, | |||
aside, | |||
details, | |||
@@ -21,8 +29,10 @@ summary { | |||
} | |||
audio, | |||
canvas, | |||
progress, | |||
video { | |||
display: inline-block; | |||
vertical-align: baseline; | |||
} | |||
audio:not([controls]) { | |||
display: none; | |||
@@ -32,29 +42,13 @@ audio:not([controls]) { | |||
template { | |||
display: none; | |||
} | |||
html { | |||
font-family: sans-serif; | |||
-ms-text-size-adjust: 100%; | |||
-webkit-text-size-adjust: 100%; | |||
} | |||
body { | |||
margin: 0; | |||
} | |||
a { | |||
background: transparent; | |||
} | |||
a:focus { | |||
outline: thin dotted; | |||
} | |||
a:active, | |||
a:hover { | |||
outline: 0; | |||
} | |||
h1 { | |||
margin: .67em 0; | |||
font-size: 2em; | |||
} | |||
abbr[title] { | |||
border-bottom: 1px dotted; | |||
} | |||
@@ -65,28 +59,14 @@ strong { | |||
dfn { | |||
font-style: italic; | |||
} | |||
hr { | |||
height: 0; | |||
-moz-box-sizing: content-box; | |||
box-sizing: content-box; | |||
h1 { | |||
margin: .67em 0; | |||
font-size: 2em; | |||
} | |||
mark { | |||
color: #000; | |||
background: #ff0; | |||
} | |||
code, | |||
kbd, | |||
pre, | |||
samp { | |||
font-family: monospace, serif; | |||
font-size: 1em; | |||
} | |||
pre { | |||
white-space: pre-wrap; | |||
} | |||
q { | |||
quotes: "\201C" "\201D" "\2018" "\2019"; | |||
} | |||
small { | |||
font-size: 80%; | |||
} | |||
@@ -110,28 +90,34 @@ svg:not(:root) { | |||
overflow: hidden; | |||
} | |||
figure { | |||
margin: 0; | |||
margin: 1em 40px; | |||
} | |||
fieldset { | |||
padding: .35em .625em .75em; | |||
margin: 0 2px; | |||
border: 1px solid #c0c0c0; | |||
hr { | |||
height: 0; | |||
-moz-box-sizing: content-box; | |||
box-sizing: content-box; | |||
} | |||
legend { | |||
padding: 0; | |||
border: 0; | |||
pre { | |||
overflow: auto; | |||
} | |||
code, | |||
kbd, | |||
pre, | |||
samp { | |||
font-family: monospace, monospace; | |||
font-size: 1em; | |||
} | |||
button, | |||
input, | |||
optgroup, | |||
select, | |||
textarea { | |||
margin: 0; | |||
font-family: inherit; | |||
font-size: 100%; | |||
font: inherit; | |||
color: inherit; | |||
} | |||
button, | |||
input { | |||
line-height: normal; | |||
button { | |||
overflow: visible; | |||
} | |||
button, | |||
select { | |||
@@ -148,11 +134,23 @@ button[disabled], | |||
html input[disabled] { | |||
cursor: default; | |||
} | |||
button::-moz-focus-inner, | |||
input::-moz-focus-inner { | |||
padding: 0; | |||
border: 0; | |||
} | |||
input { | |||
line-height: normal; | |||
} | |||
input[type="checkbox"], | |||
input[type="radio"] { | |||
box-sizing: border-box; | |||
padding: 0; | |||
} | |||
input[type="number"]::-webkit-inner-spin-button, | |||
input[type="number"]::-webkit-outer-spin-button { | |||
height: auto; | |||
} | |||
input[type="search"] { | |||
-webkit-box-sizing: content-box; | |||
-moz-box-sizing: content-box; | |||
@@ -163,19 +161,29 @@ input[type="search"]::-webkit-search-cancel-button, | |||
input[type="search"]::-webkit-search-decoration { | |||
-webkit-appearance: none; | |||
} | |||
button::-moz-focus-inner, | |||
input::-moz-focus-inner { | |||
fieldset { | |||
padding: .35em .625em .75em; | |||
margin: 0 2px; | |||
border: 1px solid #c0c0c0; | |||
} | |||
legend { | |||
padding: 0; | |||
border: 0; | |||
} | |||
textarea { | |||
overflow: auto; | |||
vertical-align: top; | |||
} | |||
optgroup { | |||
font-weight: bold; | |||
} | |||
table { | |||
border-spacing: 0; | |||
border-collapse: collapse; | |||
} | |||
td, | |||
th { | |||
padding: 0; | |||
} | |||
@media print { | |||
* { | |||
color: #000 !important; | |||
@@ -213,9 +221,6 @@ table { | |||
img { | |||
max-width: 100% !important; | |||
} | |||
@page { | |||
margin: 2cm .5cm; | |||
} | |||
p, | |||
h2, | |||
h3 { | |||
@@ -296,6 +301,9 @@ a:focus { | |||
outline: 5px auto -webkit-focus-ring-color; | |||
outline-offset: -2px; | |||
} | |||
figure { | |||
margin: 0; | |||
} | |||
img { | |||
vertical-align: middle; | |||
} | |||
@@ -1620,6 +1628,7 @@ table th[class*="col-"] { | |||
} | |||
} | |||
fieldset { | |||
min-width: 0; | |||
padding: 0; | |||
margin: 0; | |||
border: 0; | |||
@@ -1662,11 +1671,6 @@ select[multiple], | |||
select[size] { | |||
height: auto; | |||
} | |||
select optgroup { | |||
font-family: inherit; | |||
font-size: inherit; | |||
font-style: inherit; | |||
} | |||
input[type="file"]:focus, | |||
input[type="radio"]:focus, | |||
input[type="checkbox"]:focus { | |||
@@ -1674,10 +1678,6 @@ input[type="checkbox"]:focus { | |||
outline: 5px auto -webkit-focus-ring-color; | |||
outline-offset: -2px; | |||
} | |||
input[type="number"]::-webkit-outer-spin-button, | |||
input[type="number"]::-webkit-inner-spin-button { | |||
height: auto; | |||
} | |||
output { | |||
display: block; | |||
padding-top: 7px; | |||
@@ -1699,7 +1699,7 @@ output { | |||
border-radius: 4px; | |||
} | |||
.form-control:focus { | |||
border-color: #66afe9; | |||
border-color: #000; | |||
outline: 0; | |||
} | |||
.form-control:-moz-placeholder { | |||
@@ -1720,6 +1720,7 @@ output { | |||
fieldset[disabled] .form-control { | |||
cursor: not-allowed; | |||
background-color: #eee; | |||
opacity: 1; | |||
} | |||
textarea.form-control { | |||
height: auto; | |||
@@ -1794,7 +1795,8 @@ select.input-sm { | |||
height: 30px; | |||
line-height: 30px; | |||
} | |||
textarea.input-sm { | |||
textarea.input-sm, | |||
select[multiple].input-sm { | |||
height: auto; | |||
} | |||
.input-lg { | |||
@@ -1808,7 +1810,8 @@ select.input-lg { | |||
height: 46px; | |||
line-height: 46px; | |||
} | |||
textarea.input-lg { | |||
textarea.input-lg, | |||
select[multiple].input-lg { | |||
height: auto; | |||
} | |||
.has-feedback { | |||
@@ -3312,7 +3315,10 @@ select.input-group-lg > .input-group-btn > .btn { | |||
} | |||
textarea.input-group-lg > .form-control, | |||
textarea.input-group-lg > .input-group-addon, | |||
textarea.input-group-lg > .input-group-btn > .btn { | |||
textarea.input-group-lg > .input-group-btn > .btn, | |||
select[multiple].input-group-lg > .form-control, | |||
select[multiple].input-group-lg > .input-group-addon, | |||
select[multiple].input-group-lg > .input-group-btn > .btn { | |||
height: auto; | |||
} | |||
.input-group-sm > .form-control, | |||
@@ -3332,7 +3338,10 @@ select.input-group-sm > .input-group-btn > .btn { | |||
} | |||
textarea.input-group-sm > .form-control, | |||
textarea.input-group-sm > .input-group-addon, | |||
textarea.input-group-sm > .input-group-btn > .btn { | |||
textarea.input-group-sm > .input-group-btn > .btn, | |||
select[multiple].input-group-sm > .form-control, | |||
select[multiple].input-group-sm > .input-group-addon, | |||
select[multiple].input-group-sm > .input-group-btn > .btn { | |||
height: auto; | |||
} | |||
.input-group-addon, | |||
@@ -3717,6 +3726,7 @@ textarea.input-group-sm > .input-group-btn > .btn { | |||
} | |||
.navbar-brand { | |||
float: left; | |||
height: 20px; | |||
padding: 8px 15px; | |||
font-size: 18px; | |||
line-height: 20px; | |||
@@ -3725,11 +3735,6 @@ textarea.input-group-sm > .input-group-btn > .btn { | |||
.navbar-brand:focus { | |||
text-decoration: none; | |||
} | |||
.navbar-brand > .glyphicon { | |||
float: left; | |||
margin-top: -2px; | |||
margin-right: 5px; | |||
} | |||
@media (min-width: 768px) { | |||
.navbar > .container .navbar-brand, | |||
.navbar > .container-fluid .navbar-brand { | |||
@@ -4834,8 +4839,8 @@ a.list-group-item-danger.active:focus { | |||
.panel > .panel-body + .table-responsive { | |||
border-top: 1px solid #ddd; | |||
} | |||
.panel > .table > tbody:first-child th, | |||
.panel > .table > tbody:first-child td { | |||
.panel > .table > tbody:first-child > tr:first-child th, | |||
.panel > .table > tbody:first-child > tr:first-child td { | |||
border-top: 0; | |||
} | |||
.panel > .table-bordered, | |||
@@ -32,6 +32,14 @@ | |||
font-weight: normal; | |||
font-style: normal; | |||
} | |||
@media screen and (-webkit-min-device-pixel-ratio:0) { | |||
@font-face { | |||
font-family: 'FontAwesome'; | |||
src: url('../lib/css/font/fontawesome-webfont.svg#fontawesomeregular?v=3.2.1') format('svg'); | |||
} | |||
} | |||
/* FONT AWESOME CORE | |||
* -------------------------- */ | |||
[class^="icon-"], | |||
@@ -0,0 +1,9 @@ | |||
// include in bootstrap.less | |||
@font-family-sans-serif: Arial, sans-serif; | |||
@input-border-focus: #000; | |||
@alert-padding: 10px; | |||
label { | |||
font-weight: normal; | |||
} |
@@ -389,7 +389,7 @@ $.extend(_p, { | |||
lh = cstr(wn.boot.letter_heads[cur_frm.doc.letter_head]); | |||
} else if (cp.letter_head) { | |||
lh = cp.letter_head; | |||
} | |||
} | |||
return lh; | |||
}, | |||
@@ -1,5 +1,5 @@ | |||
/*! | |||
* Bootstrap v3.0.3 (http://getbootstrap.com) | |||
* Bootstrap v3.1.0 (http://getbootstrap.com) | |||
* Copyright 2011-2014 Twitter, Inc. | |||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) | |||
*/ |
@@ -100,13 +100,8 @@ $.extend(wn.meta, { | |||
}, | |||
get_print_formats: function(doctype) { | |||
// if default print format is given, use it | |||
var print_format_list = []; | |||
if(locals.DocType[doctype].default_print_format) | |||
print_format_list.push(locals.DocType[doctype].default_print_format) | |||
if(!in_list(print_format_list, "Standard")) | |||
print_format_list.push("Standard"); | |||
var print_format_list = ["Standard"]; | |||
var default_print_format = locals.DocType[doctype].default_print_format; | |||
var print_formats = wn.model.get("Print Format", {doc_type: doctype}) | |||
.sort(function(a, b) { return (a > b) ? 1 : -1; }); | |||
@@ -114,6 +109,12 @@ $.extend(wn.meta, { | |||
if(!in_list(print_format_list, d.name)) | |||
print_format_list.push(d.name); | |||
}); | |||
if(default_print_format && default_print_format != "Standard") { | |||
var index = print_format_list.indexOf(default_print_format) - 1; | |||
print_format_list.sort().splice(index, 1); | |||
print_format_list.unshift(default_print_format); | |||
} | |||
return print_format_list; | |||
}, | |||
@@ -125,7 +125,7 @@ bsEditor = Class.extend({ | |||
clean_html: function() { | |||
var html = this.editor.html() || ""; | |||
if(!strip(this.editor.text())) html = ""; | |||
if(!strip(this.editor.text()) && !(this.editor.find("img"))) html = ""; | |||
// html = html.replace(/(<br>|\s|<div><br><\/div>| )*$/, ''); | |||
// remove custom typography (use CSS!) | |||
@@ -0,0 +1,5 @@ | |||
<h3>New Message</h3> | |||
<p>You have a new message from: <b>{{ from }}</b></p> | |||
<p>{{ message }}</p> | |||
<hr> | |||
<p><a href="{{ link }}">Login and view in Browser</a></p> |
@@ -0,0 +1,10 @@ | |||
<h3>{{ title }}</h3> | |||
<p>Dear {{ first_name }}{% if last_name %} {{ last_name}}{% endif %},</p> | |||
<p>A new account has been created for you.</p> | |||
<p>Your login id is: <b>{{ user }}</b> | |||
<p>Click on the button below to complete your registration and set a new password.</p> | |||
<p><a class="btn-primary" href="{{ link }}">Complete Registration</a></p> | |||
<br> | |||
<p>You can also copy-paste this link in your browser <a href="{{ link }}">{{ link }}</a></p> | |||
<p>Thank you,<br> | |||
{{ user_fullname }}</p> |
@@ -0,0 +1,6 @@ | |||
<h3>Password Reset</h3> | |||
<p>Dear {{ first_name }}{% if last_name %} {{ last_name}}{% endif %},</p> | |||
<p>Please click on the following link to update your new password:</p> | |||
<p><a href="{{ link }}">{{ link }}</a></p> | |||
<p>Thank you,<br> | |||
{{ user_fullname }}</p> |
@@ -0,0 +1,5 @@ | |||
<h3>Password Update Notification</h3> | |||
<p>Dear {{ first_name }}{% if last_name %} {{ last_name}}{% endif %},</p> | |||
<p>Your password has been updated. Here is your new password: {{ new_password }}</p> | |||
<p>Thank you,<br> | |||
{{ user_fullname }}</p> |
@@ -25,9 +25,9 @@ body { | |||
-webkit-text-size-adjust:none; | |||
width: 100%!important; | |||
height: 100%; | |||
background-color: #f6f6f6; | |||
} | |||
/* ------------------------------------- | |||
ELEMENTS | |||
------------------------------------- */ | |||
@@ -115,7 +115,7 @@ table.footer-wrap a{ | |||
------------------------------------- */ | |||
h1,h2,h3{ | |||
font-family: "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; line-height: 1.1; margin-bottom:15px; color:#000; | |||
margin: 40px 0 10px; | |||
margin: 20px 0 10px; | |||
line-height: 1.2; | |||
font-weight:200; | |||
} | |||
@@ -129,7 +129,10 @@ h2 { | |||
h3 { | |||
font-size: 22px; | |||
} | |||
hr { | |||
margin: 20px 0; | |||
border-top: 1px solid #eee; | |||
} | |||
p, ul, ol { | |||
margin-bottom: 10px; | |||
font-weight: normal; |
@@ -30,7 +30,7 @@ def main(app=None, module=None, doctype=None, verbose=False): | |||
for doctype in module.test_dependencies: | |||
make_test_records(doctype, verbose=verbose) | |||
test_suite.addTest(unittest.TestLoader().loadTestsFromModule(sys.modules[module])) | |||
test_suite.addTest(unittest.TestLoader().loadTestsFromModule(module)) | |||
ret = unittest.TextTestRunner(verbosity=1+(verbose and 1 or 0)).run(test_suite) | |||
else: | |||
ret = run_all_tests(app, verbose) | |||
@@ -4,10 +4,6 @@ | |||
from __future__ import unicode_literals | |||
import os, sys | |||
sys.path.append('.') | |||
sys.path.append('lib/py') | |||
sys.path.append('erpnext') | |||
import unittest, webnotes | |||
from webnotes.test_runner import make_test_records | |||
@@ -288,4 +288,4 @@ def google_translate(lang, untranslated): | |||
return dict(zip(untranslated, translated)) | |||
else: | |||
print "unable to translate" | |||
return {} | |||
return {} |
@@ -5,9 +5,11 @@ | |||
from __future__ import unicode_literals | |||
from werkzeug.test import Client | |||
import os | |||
import re | |||
import urllib | |||
import webnotes | |||
import os | |||
no_value_fields = ['Section Break', 'Column Break', 'HTML', 'Table', 'FlexTable', | |||
'Button', 'Image', 'Graph'] | |||
@@ -64,7 +66,6 @@ def extract_email_id(email): | |||
def validate_email_add(email_str): | |||
"""Validates the email string""" | |||
email = extract_email_id(email_str) | |||
import re | |||
return re.match("[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?", email.lower()) | |||
def get_request_site_address(full_address=False): | |||
@@ -277,7 +278,6 @@ def dict_to_str(args, sep='&'): | |||
""" | |||
Converts a dictionary to URL | |||
""" | |||
import urllib | |||
t = [] | |||
for k in args.keys(): | |||
t.append(str(k)+'='+urllib.quote(str(args[k] or ''))) | |||
@@ -670,7 +670,6 @@ def strip_html(text): | |||
""" | |||
removes anything enclosed in and including <> | |||
""" | |||
import re | |||
return re.compile(r'<.*?>').sub('', text) | |||
def escape_html(text): | |||
@@ -831,7 +830,6 @@ def get_url(uri=None): | |||
url = "http://" + subdomain | |||
if uri: | |||
import urllib | |||
url = urllib.basejoin(url, uri) | |||
return url | |||
@@ -896,14 +894,26 @@ def get_disk_usage(): | |||
return 0 | |||
err, out = execute_in_shell("du -hsm {files_path}".format(files_path=files_path)) | |||
return cint(out.split("\n")[-2].split("\t")[0]) | |||
def expand_partial_links(html): | |||
import re | |||
def scrub_urls(html): | |||
html = expand_relative_urls(html) | |||
html = quote_urls(html) | |||
return html | |||
def expand_relative_urls(html): | |||
# expand relative urls | |||
url = get_url() | |||
if not url.endswith("/"): url += "/" | |||
return re.sub('(href|src){1}([\s]*=[\s]*[\'"]?)/*((?!http)[^\'" >]+)([\'"]?)', | |||
'\g<1>\g<2>{}\g<3>\g<4>'.format(url), | |||
html) | |||
return re.sub('(href|src){1}([\s]*=[\s]*[\'"]?)((?!http)[^\'" >]+)([\'"]?)', | |||
'\g<1>\g<2>{}\g<3>\g<4>'.format(url), html) | |||
def quote_urls(html): | |||
def _quote_url(match): | |||
groups = list(match.groups()) | |||
groups[2] = urllib.quote(groups[2], safe="/:") | |||
return "".join(groups) | |||
return re.sub('(href|src){1}([\s]*=[\s]*[\'"]?)((?:http)[^\'">]+)([\'"]?)', | |||
_quote_url, html) | |||
def touch_file(path): | |||
with open(path, 'a'): | |||
@@ -912,4 +922,4 @@ def touch_file(path): | |||
def get_test_client(): | |||
from webnotes.app import application | |||
return Client(application) | |||
return Client(application) |
@@ -5,6 +5,9 @@ from __future__ import unicode_literals | |||
import webnotes | |||
from webnotes import conf | |||
from webnotes.utils.email_lib.email_body import get_email | |||
from webnotes.utils.email_lib.smtp import send | |||
def sendmail_md(recipients, sender=None, msg=None, subject=None): | |||
"""send markdown email""" | |||
import markdown2 | |||
@@ -12,12 +15,11 @@ def sendmail_md(recipients, sender=None, msg=None, subject=None): | |||
def sendmail(recipients, sender='', msg='', subject='[No Subject]'): | |||
"""send an html email as multipart with attachments and all""" | |||
from webnotes.utils.email_lib.smtp import get_email | |||
get_email(recipients, sender, msg, subject).send() | |||
send(get_email(recipients, sender, msg, subject)) | |||
def sendmail_to_system_managers(subject, content): | |||
from webnotes.utils.email_lib.smtp import get_email | |||
get_email(get_system_managers(), None, content, subject).send() | |||
send(get_email(get_system_managers(), None, content, subject)) | |||
@webnotes.whitelist() | |||
def get_contact_list(): | |||
@@ -3,31 +3,29 @@ | |||
from __future__ import unicode_literals | |||
import webnotes | |||
from webnotes import msgprint, throw, _ | |||
from webnotes.model.doc import Document | |||
from webnotes.utils import cint, get_url | |||
import HTMLParser | |||
import urllib | |||
from webnotes import msgprint, throw, _ | |||
from webnotes.utils.email_lib.smtp import SMTPServer, send | |||
from webnotes.utils.email_lib.email_body import get_email, get_formatted_html | |||
from webnotes.utils.email_lib.html2text import html2text | |||
from webnotes.utils import cint, get_url, nowdate | |||
class BulkLimitCrossedError(webnotes.ValidationError): pass | |||
def send(recipients=None, sender=None, doctype='Profile', email_field='email', | |||
subject='[No Subject]', message='[No Content]', ref_doctype=None, ref_docname=None, | |||
add_unsubscribe_link=True): | |||
"""send bulk mail if not unsubscribed and within conf.bulk_mail_limit""" | |||
import webnotes | |||
def is_unsubscribed(rdata): | |||
if not rdata: return 1 | |||
if not rdata: | |||
return 1 | |||
return cint(rdata.unsubscribed) | |||
def check_bulk_limit(new_mails): | |||
from webnotes import conf | |||
from webnotes.utils import nowdate | |||
this_month = webnotes.conn.sql("""select count(*) from `tabBulk Email` where | |||
month(creation)=month(%s)""" % nowdate())[0][0] | |||
monthly_bulk_mail_limit = conf.get('monthly_bulk_mail_limit') or 500 | |||
monthly_bulk_mail_limit = webnotes.conf.get('monthly_bulk_mail_limit') or 500 | |||
if this_month + len(recipients) > monthly_bulk_mail_limit: | |||
throw("{bulk} ({limit}) {cross}".format(**{ | |||
@@ -36,10 +34,10 @@ def send(recipients=None, sender=None, doctype='Profile', email_field='email', | |||
"cross": _("crossed") | |||
}), exc=BulkLimitCrossedError) | |||
def update_message(doc): | |||
updated = message | |||
def update_message(formatted, doc, add_unsubscribe_link): | |||
updated = formatted | |||
if add_unsubscribe_link: | |||
updated += """<div style="padding: 7px; border-top: 1px solid #aaa; | |||
unsubscribe_link = """<div style="padding: 7px; border-top: 1px solid #aaa; | |||
margin-top: 17px;"> | |||
<small><a href="%s/?%s"> | |||
Unsubscribe</a> from this list.</small></div>""" % (get_url(), | |||
@@ -49,6 +47,8 @@ def send(recipients=None, sender=None, doctype='Profile', email_field='email', | |||
"type": doctype, | |||
"email_field": email_field | |||
})) | |||
updated = updated.replace("<!--unsubscribe link here-->", unsubscribe_link) | |||
return updated | |||
@@ -56,36 +56,33 @@ def send(recipients=None, sender=None, doctype='Profile', email_field='email', | |||
if not sender or sender == "Administrator": | |||
sender = webnotes.conn.get_value('Email Settings', None, 'auto_email_id') | |||
check_bulk_limit(len(recipients)) | |||
import HTMLParser | |||
from webnotes.utils.email_lib.html2text import html2text | |||
from webnotes.utils import expand_partial_links | |||
try: | |||
message = expand_partial_links(message) | |||
text_content = html2text(message) | |||
except HTMLParser.HTMLParseError: | |||
text_content = "[See html attachment]" | |||
formatted = get_formatted_html(subject, message) | |||
for r in filter(None, list(set(recipients))): | |||
rdata = webnotes.conn.sql("""select * from `tab%s` where %s=%s""" % (doctype, | |||
email_field, '%s'), (r,), as_dict=1) | |||
doc = rdata and rdata[0] or {} | |||
if not is_unsubscribed(doc): | |||
# add to queue | |||
add(r, sender, subject, update_message(doc), text_content, ref_doctype, ref_docname) | |||
add(r, sender, subject, update_message(formatted, doc, add_unsubscribe_link), | |||
text_content, ref_doctype, ref_docname) | |||
def add(email, sender, subject, message, text_content=None, ref_doctype=None, ref_docname=None): | |||
"""add to bulk mail queue""" | |||
from webnotes.utils.email_lib.smtp import get_email | |||
e = Document('Bulk Email') | |||
def add(email, sender, subject, formatted, text_content=None, | |||
ref_doctype=None, ref_docname=None): | |||
"""add to bulk mail queue""" | |||
e = webnotes.doc('Bulk Email') | |||
e.sender = sender | |||
e.recipient = email | |||
try: | |||
e.message = get_email(email, sender=e.sender, msg=message, subject=subject, | |||
e.message = get_email(email, sender=e.sender, formatted=formatted, subject=subject, | |||
text_content = text_content).as_string() | |||
except webnotes.ValidationError: | |||
# bad email id - don't add to queue | |||
@@ -116,15 +113,11 @@ def unsubscribe(): | |||
def flush(from_test=False): | |||
"""flush email queue, every time: called from scheduler""" | |||
import webnotes | |||
from webnotes import conf | |||
from webnotes.utils.email_lib.smtp import SMTPServer, get_email | |||
smptserver = SMTPServer() | |||
auto_commit = not from_test | |||
if webnotes.flags.mute_emails or conf.get("mute_emails") or False: | |||
if webnotes.flags.mute_emails or webnotes.conf.get("mute_emails") or False: | |||
msgprint(_("Emails are muted")) | |||
from_test = True | |||
@@ -0,0 +1,217 @@ | |||
# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors | |||
# MIT License. See license.txt | |||
from __future__ import unicode_literals | |||
import webnotes | |||
from webnotes.utils import scrub_urls | |||
import email.utils | |||
from inlinestyler.utils import inline_css | |||
def get_email(recipients, sender='', msg='', subject='[No Subject]', | |||
text_content = None, footer=None, formatted=None): | |||
"""send an html email as multipart with attachments and all""" | |||
email = EMail(sender, recipients, subject) | |||
if (not '<br>' in msg) and (not '<p>' in msg) and (not '<div' in msg): | |||
msg = msg.replace('\n', '<br>') | |||
email.set_html(msg, text_content, footer=footer, formatted=formatted) | |||
return email | |||
class EMail: | |||
""" | |||
Wrapper on the email module. Email object represents emails to be sent to the client. | |||
Also provides a clean way to add binary `FileData` attachments | |||
Also sets all messages as multipart/alternative for cleaner reading in text-only clients | |||
""" | |||
def __init__(self, sender='', recipients=[], subject='', alternative=0, reply_to=None): | |||
from email.mime.multipart import MIMEMultipart | |||
from email import Charset | |||
Charset.add_charset('utf-8', Charset.QP, Charset.QP, 'utf-8') | |||
if isinstance(recipients, basestring): | |||
recipients = recipients.replace(';', ',').replace('\n', '') | |||
recipients = recipients.split(',') | |||
# remove null | |||
recipients = filter(None, (r.strip() for r in recipients)) | |||
self.sender = sender | |||
self.reply_to = reply_to or sender | |||
self.recipients = recipients | |||
self.subject = subject | |||
self.msg_root = MIMEMultipart('mixed') | |||
self.msg_multipart = MIMEMultipart('alternative') | |||
self.msg_root.attach(self.msg_multipart) | |||
self.cc = [] | |||
self.html_set = False | |||
def set_html(self, message, text_content = None, footer=None, formatted=None): | |||
"""Attach message in the html portion of multipart/alternative""" | |||
if not formatted: | |||
formatted = get_formatted_html(self.subject, message, footer) | |||
# this is the first html part of a multi-part message, | |||
# convert to text well | |||
if not self.html_set: | |||
if text_content: | |||
self.set_text(text_content) | |||
else: | |||
self.set_html_as_text(message) | |||
self.set_part_html(formatted) | |||
self.html_set = True | |||
def set_text(self, message): | |||
""" | |||
Attach message in the text portion of multipart/alternative | |||
""" | |||
from email.mime.text import MIMEText | |||
part = MIMEText(message.encode('utf-8'), 'plain', 'utf-8') | |||
self.msg_multipart.attach(part) | |||
def set_part_html(self, message): | |||
from email.mime.text import MIMEText | |||
part = MIMEText(message.encode('utf-8'), 'html', 'utf-8') | |||
self.msg_multipart.attach(part) | |||
def set_html_as_text(self, html): | |||
"""return html2text""" | |||
import HTMLParser | |||
from webnotes.utils.email_lib.html2text import html2text | |||
try: | |||
self.set_text(html2text(html)) | |||
except HTMLParser.HTMLParseError: | |||
pass | |||
def set_message(self, message, mime_type='text/html', as_attachment=0, filename='attachment.html'): | |||
"""Append the message with MIME content to the root node (as attachment)""" | |||
from email.mime.text import MIMEText | |||
maintype, subtype = mime_type.split('/') | |||
part = MIMEText(message, _subtype = subtype) | |||
if as_attachment: | |||
part.add_header('Content-Disposition', 'attachment', filename=filename) | |||
self.msg_root.attach(part) | |||
def attach_file(self, n): | |||
"""attach a file from the `FileData` table""" | |||
from webnotes.utils.file_manager import get_file | |||
res = get_file(n) | |||
if not res: | |||
return | |||
self.add_attachment(res[0], res[1]) | |||
def add_attachment(self, fname, fcontent, content_type=None): | |||
"""add attachment""" | |||
from email.mime.audio import MIMEAudio | |||
from email.mime.base import MIMEBase | |||
from email.mime.image import MIMEImage | |||
from email.mime.text import MIMEText | |||
import mimetypes | |||
if not content_type: | |||
content_type, encoding = mimetypes.guess_type(fname) | |||
if content_type is None: | |||
# No guess could be made, or the file is encoded (compressed), so | |||
# use a generic bag-of-bits type. | |||
content_type = 'application/octet-stream' | |||
maintype, subtype = content_type.split('/', 1) | |||
if maintype == 'text': | |||
# Note: we should handle calculating the charset | |||
if isinstance(fcontent, unicode): | |||
fcontent = fcontent.encode("utf-8") | |||
part = MIMEText(fcontent, _subtype=subtype, _charset="utf-8") | |||
elif maintype == 'image': | |||
part = MIMEImage(fcontent, _subtype=subtype) | |||
elif maintype == 'audio': | |||
part = MIMEAudio(fcontent, _subtype=subtype) | |||
else: | |||
part = MIMEBase(maintype, subtype) | |||
part.set_payload(fcontent) | |||
# Encode the payload using Base64 | |||
from email import encoders | |||
encoders.encode_base64(part) | |||
# Set the filename parameter | |||
if fname: | |||
part.add_header(b'Content-Disposition', | |||
("attachment; filename=%s" % fname).encode('utf-8')) | |||
self.msg_root.attach(part) | |||
def validate(self): | |||
"""validate the email ids""" | |||
from webnotes.utils import validate_email_add | |||
def _validate(email): | |||
"""validate an email field""" | |||
if email and not validate_email_add(email): | |||
webnotes.msgprint("%s is not a valid email id" % email, | |||
raise_exception = 1) | |||
return email | |||
if not self.sender: | |||
self.sender = webnotes.conn.get_value('Email Settings', None, | |||
'auto_email_id') or webnotes.conf.get('auto_email_id') or None | |||
if not self.sender: | |||
webnotes.msgprint("""Please specify 'Auto Email Id' \ | |||
in Setup > Email Settings""") | |||
if not "expires_on" in webnotes.conf: | |||
webnotes.msgprint("""Alternatively, \ | |||
you can also specify 'auto_email_id' in site_config.json""") | |||
raise webnotes.ValidationError | |||
self.sender = _validate(self.sender) | |||
self.reply_to = _validate(self.reply_to) | |||
for e in self.recipients + (self.cc or []): | |||
_validate(e.strip()) | |||
def make(self): | |||
"""build into msg_root""" | |||
self.msg_root['Subject'] = self.subject.encode("utf-8") | |||
self.msg_root['From'] = self.sender.encode("utf-8") | |||
self.msg_root['To'] = ', '.join([r.strip() for r in self.recipients]).encode("utf-8") | |||
self.msg_root['Date'] = email.utils.formatdate() | |||
if not self.reply_to: | |||
self.reply_to = self.sender | |||
self.msg_root['Reply-To'] = self.reply_to.encode("utf-8") | |||
if self.cc: | |||
self.msg_root['CC'] = ', '.join([r.strip() for r in self.cc]).encode("utf-8") | |||
def as_string(self): | |||
"""validate, build message and convert to string""" | |||
self.validate() | |||
self.make() | |||
return self.msg_root.as_string() | |||
def get_formatted_html(subject, message, footer=None): | |||
message = scrub_urls(message) | |||
return inline_css(webnotes.get_template("templates/emails/standard.html").render({ | |||
"content": message, | |||
"footer": get_footer(footer), | |||
"title": subject | |||
})) | |||
def get_footer(footer=None): | |||
"""append a footer (signature)""" | |||
footer = footer or "" | |||
# control panel | |||
footer += webnotes.conn.get_value('Control Panel',None,'mail_footer') or '' | |||
# hooks | |||
for f in webnotes.get_hooks("mail_footer"): | |||
footer += webnotes.get_attr(f) | |||
footer += "<!--unsubscribe link here-->" | |||
return footer |
@@ -2,247 +2,43 @@ | |||
# MIT License. See license.txt | |||
from __future__ import unicode_literals | |||
""" | |||
Sends email via outgoing server specified in "Control Panel" | |||
Allows easy adding of Attachments of "File" objects | |||
""" | |||
import webnotes | |||
from webnotes import conf | |||
from webnotes import msgprint | |||
from webnotes.utils import cint, expand_partial_links | |||
import email.utils | |||
def get_email(recipients, sender='', msg='', subject='[No Subject]', text_content = None, footer=None): | |||
"""send an html email as multipart with attachments and all""" | |||
email = EMail(sender, recipients, subject) | |||
if (not '<br>' in msg) and (not '<p>' in msg) and (not '<div' in msg): | |||
msg = msg.replace('\n', '<br>') | |||
email.set_html(msg, text_content, footer=footer) | |||
return email | |||
class EMail: | |||
""" | |||
Wrapper on the email module. Email object represents emails to be sent to the client. | |||
Also provides a clean way to add binary `FileData` attachments | |||
Also sets all messages as multipart/alternative for cleaner reading in text-only clients | |||
""" | |||
def __init__(self, sender='', recipients=[], subject='', alternative=0, reply_to=None): | |||
from email.mime.multipart import MIMEMultipart | |||
from email import Charset | |||
Charset.add_charset('utf-8', Charset.QP, Charset.QP, 'utf-8') | |||
if isinstance(recipients, basestring): | |||
recipients = recipients.replace(';', ',').replace('\n', '') | |||
recipients = recipients.split(',') | |||
# remove null | |||
recipients = filter(None, (r.strip() for r in recipients)) | |||
self.sender = sender | |||
self.reply_to = reply_to or sender | |||
self.recipients = recipients | |||
self.subject = subject | |||
self.msg_root = MIMEMultipart('mixed') | |||
self.msg_multipart = MIMEMultipart('alternative') | |||
self.msg_root.attach(self.msg_multipart) | |||
self.cc = [] | |||
self.html_set = False | |||
def set_html(self, message, text_content = None, footer=None): | |||
"""Attach message in the html portion of multipart/alternative""" | |||
message = message + self.get_footer(footer) | |||
message = expand_partial_links(message) | |||
# this is the first html part of a multi-part message, | |||
# convert to text well | |||
if not self.html_set: | |||
if text_content: | |||
self.set_text(text_content) | |||
else: | |||
self.set_html_as_text(message) | |||
self.set_part_html(message) | |||
self.html_set = True | |||
def set_text(self, message): | |||
""" | |||
Attach message in the text portion of multipart/alternative | |||
""" | |||
from email.mime.text import MIMEText | |||
part = MIMEText(message.encode('utf-8'), 'plain', 'utf-8') | |||
self.msg_multipart.attach(part) | |||
def set_part_html(self, message): | |||
from email.mime.text import MIMEText | |||
part = MIMEText(message.encode('utf-8'), 'html', 'utf-8') | |||
self.msg_multipart.attach(part) | |||
def set_html_as_text(self, html): | |||
"""return html2text""" | |||
import HTMLParser | |||
from webnotes.utils.email_lib.html2text import html2text | |||
try: | |||
self.set_text(html2text(html)) | |||
except HTMLParser.HTMLParseError: | |||
pass | |||
def set_message(self, message, mime_type='text/html', as_attachment=0, filename='attachment.html'): | |||
"""Append the message with MIME content to the root node (as attachment)""" | |||
from email.mime.text import MIMEText | |||
maintype, subtype = mime_type.split('/') | |||
part = MIMEText(message, _subtype = subtype) | |||
if as_attachment: | |||
part.add_header('Content-Disposition', 'attachment', filename=filename) | |||
self.msg_root.attach(part) | |||
def get_footer(self, footer=None): | |||
"""append a footer (signature)""" | |||
footer = footer or "" | |||
footer += webnotes.conn.get_value('Control Panel',None,'mail_footer') or '' | |||
other_footers = webnotes.get_hooks().mail_footer or [] | |||
for f in other_footers: | |||
footer += f | |||
return footer | |||
def attach_file(self, n): | |||
"""attach a file from the `FileData` table""" | |||
from webnotes.utils.file_manager import get_file | |||
res = get_file(n) | |||
if not res: | |||
return | |||
self.add_attachment(res[0], res[1]) | |||
import smtplib | |||
import _socket | |||
from webnotes.utils import cint | |||
def send(email, as_bulk=False): | |||
"""send the message or add it to Outbox Email""" | |||
if webnotes.flags.mute_emails or webnotes.conf.get("mute_emails") or False: | |||
webnotes.msgprint("Emails are muted") | |||
return | |||
def add_attachment(self, fname, fcontent, content_type=None): | |||
"""add attachment""" | |||
from email.mime.audio import MIMEAudio | |||
from email.mime.base import MIMEBase | |||
from email.mime.image import MIMEImage | |||
from email.mime.text import MIMEText | |||
import mimetypes | |||
if not content_type: | |||
content_type, encoding = mimetypes.guess_type(fname) | |||
if content_type is None: | |||
# No guess could be made, or the file is encoded (compressed), so | |||
# use a generic bag-of-bits type. | |||
content_type = 'application/octet-stream' | |||
maintype, subtype = content_type.split('/', 1) | |||
if maintype == 'text': | |||
# Note: we should handle calculating the charset | |||
if isinstance(fcontent, unicode): | |||
fcontent = fcontent.encode("utf-8") | |||
part = MIMEText(fcontent, _subtype=subtype, _charset="utf-8") | |||
elif maintype == 'image': | |||
part = MIMEImage(fcontent, _subtype=subtype) | |||
elif maintype == 'audio': | |||
part = MIMEAudio(fcontent, _subtype=subtype) | |||
else: | |||
part = MIMEBase(maintype, subtype) | |||
part.set_payload(fcontent) | |||
# Encode the payload using Base64 | |||
from email import encoders | |||
encoders.encode_base64(part) | |||
try: | |||
smtpserver = SMTPServer() | |||
if hasattr(smtpserver, "always_use_login_id_as_sender") and \ | |||
cint(smtpserver.always_use_login_id_as_sender) and smtpserver.login: | |||
if not email.reply_to: | |||
email.reply_to = email.sender | |||
email.sender = smtpserver.login | |||
smtpserver.sess.sendmail(email.sender, email.recipients + (email.cc or []), | |||
email.as_string()) | |||
# Set the filename parameter | |||
if fname: | |||
part.add_header(b'Content-Disposition', | |||
("attachment; filename=%s" % fname).encode('utf-8')) | |||
self.msg_root.attach(part) | |||
def validate(self): | |||
"""validate the email ids""" | |||
from webnotes.utils import validate_email_add | |||
def _validate(email): | |||
"""validate an email field""" | |||
if email and not validate_email_add(email): | |||
webnotes.msgprint("%s is not a valid email id" % email, | |||
raise_exception = 1) | |||
return email | |||
if not self.sender: | |||
self.sender = webnotes.conn.get_value('Email Settings', None, | |||
'auto_email_id') or conf.get('auto_email_id') or None | |||
if not self.sender: | |||
webnotes.msgprint("""Please specify 'Auto Email Id' \ | |||
in Setup > Email Settings""") | |||
if not "expires_on" in conf: | |||
webnotes.msgprint("""Alternatively, \ | |||
you can also specify 'auto_email_id' in conf.py""") | |||
raise webnotes.ValidationError | |||
self.sender = _validate(self.sender) | |||
self.reply_to = _validate(self.reply_to) | |||
for e in self.recipients + (self.cc or []): | |||
_validate(e.strip()) | |||
def make(self): | |||
"""build into msg_root""" | |||
self.msg_root['Subject'] = self.subject.encode("utf-8") | |||
self.msg_root['From'] = self.sender.encode("utf-8") | |||
self.msg_root['To'] = ', '.join([r.strip() for r in self.recipients]).encode("utf-8") | |||
self.msg_root['Date'] = email.utils.formatdate() | |||
if not self.reply_to: | |||
self.reply_to = self.sender | |||
self.msg_root['Reply-To'] = self.reply_to.encode("utf-8") | |||
if self.cc: | |||
self.msg_root['CC'] = ', '.join([r.strip() for r in self.cc]).encode("utf-8") | |||
def as_string(self): | |||
"""validate, build message and convert to string""" | |||
self.validate() | |||
self.make() | |||
return self.msg_root.as_string() | |||
def send(self, as_bulk=False): | |||
"""send the message or add it to Outbox Email""" | |||
if webnotes.flags.mute_emails or conf.get("mute_emails") or False: | |||
webnotes.msgprint("Emails are muted") | |||
return | |||
import smtplib | |||
try: | |||
smtpserver = SMTPServer() | |||
if hasattr(smtpserver, "always_use_login_id_as_sender") and \ | |||
cint(smtpserver.always_use_login_id_as_sender) and smtpserver.login: | |||
if not self.reply_to: | |||
self.reply_to = self.sender | |||
self.sender = smtpserver.login | |||
smtpserver.sess.sendmail(self.sender, self.recipients + (self.cc or []), | |||
self.as_string()) | |||
except smtplib.SMTPSenderRefused: | |||
webnotes.msgprint("""Invalid Outgoing Mail Server's Login Id or Password. \ | |||
Please rectify and try again.""") | |||
raise | |||
except smtplib.SMTPRecipientsRefused: | |||
webnotes.msgprint("""Invalid Recipient (To) Email Address. \ | |||
Please rectify and try again.""") | |||
raise | |||
except smtplib.SMTPSenderRefused: | |||
webnotes.msgprint("""Invalid Outgoing Mail Server's Login Id or Password. \ | |||
Please rectify and try again.""") | |||
raise | |||
except smtplib.SMTPRecipientsRefused: | |||
webnotes.msgprint("""Invalid Recipient (To) Email Address. \ | |||
Please rectify and try again.""") | |||
raise | |||
class SMTPServer: | |||
def __init__(self, login=None, password=None, server=None, port=None, use_ssl=None): | |||
import webnotes.model.doc | |||
from webnotes.utils import cint | |||
# get defaults from control panel | |||
try: | |||
es = webnotes.model.doc.Document('Email Settings','Email Settings') | |||
es = webnotes.doc('Email Settings','Email Settings') | |||
except webnotes.DoesNotExistError: | |||
es = None | |||
@@ -261,11 +57,11 @@ class SMTPServer: | |||
self.password = es.mail_password | |||
self.always_use_login_id_as_sender = es.always_use_login_id_as_sender | |||
else: | |||
self.server = conf.get("mail_server") or "" | |||
self.port = conf.get("mail_port") or None | |||
self.use_ssl = cint(conf.get("use_ssl") or 0) | |||
self.login = conf.get("mail_login") or "" | |||
self.password = conf.get("mail_password") or "" | |||
self.server = webnotes.conf.get("mail_server") or "" | |||
self.port = webnotes.conf.get("mail_port") or None | |||
self.use_ssl = cint(webnotes.conf.get("use_ssl") or 0) | |||
self.login = webnotes.conf.get("mail_login") or "" | |||
self.password = webnotes.conf.get("mail_password") or "" | |||
@property | |||
def sess(self): | |||
@@ -273,10 +69,6 @@ class SMTPServer: | |||
if self._sess: | |||
return self._sess | |||
from webnotes.utils import cint | |||
import smtplib | |||
import _socket | |||
# check if email server specified | |||
if not self.server: | |||
err_msg = 'Outgoing Mail Server not specified' | |||
@@ -306,7 +98,7 @@ class SMTPServer: | |||
# check if logged correctly | |||
if ret[0]!=235: | |||
msgprint(ret[1]) | |||
webnotes.msgprint(ret[1]) | |||
raise webnotes.OutgoingEmailError, ret[1] | |||
return self._sess | |||