Parcourir la source

merge conflict fixed

version-14
Akhilesh Darjee il y a 11 ans
Parent
révision
26212d7982
31 fichiers modifiés avec 537 ajouts et 507 suppressions
  1. +40
    -0
      config.json
  2. +1
    -0
      requirements.txt
  3. +7
    -0
      webnotes/__init__.py
  4. +9
    -36
      webnotes/core/doctype/communication/communication.py
  5. +13
    -7
      webnotes/core/doctype/print_format/print_format.py
  6. +1
    -1
      webnotes/core/doctype/profile/profile.js
  7. +8
    -59
      webnotes/core/doctype/profile/profile.py
  8. +10
    -20
      webnotes/core/page/messages/messages.py
  9. +2
    -0
      webnotes/patches.txt
  10. +80
    -75
      webnotes/public/css/bootstrap.css
  11. +8
    -0
      webnotes/public/css/font-awesome.css
  12. +9
    -0
      webnotes/public/css/less/webnotes.less
  13. +1
    -1
      webnotes/public/js/legacy/print_format.js
  14. +1
    -1
      webnotes/public/js/lib/bootstrap.min.js
  15. +8
    -7
      webnotes/public/js/wn/model/meta.js
  16. +1
    -1
      webnotes/public/js/wn/ui/editor.js
  17. +0
    -0
      webnotes/templates/emails/.txt
  18. +5
    -0
      webnotes/templates/emails/new_message.html
  19. +10
    -0
      webnotes/templates/emails/new_user.html
  20. +6
    -0
      webnotes/templates/emails/password_reset.html
  21. +5
    -0
      webnotes/templates/emails/password_update.html
  22. +6
    -3
      webnotes/templates/emails/standard.html
  23. +0
    -0
      webnotes/templates/print_formats/standard.html
  24. +1
    -1
      webnotes/test_runner.py
  25. +0
    -4
      webnotes/tests/test_email.py
  26. +1
    -1
      webnotes/translate.py
  27. +22
    -12
      webnotes/utils/__init__.py
  28. +6
    -4
      webnotes/utils/email_lib/__init__.py
  29. +25
    -32
      webnotes/utils/email_lib/bulk.py
  30. +217
    -0
      webnotes/utils/email_lib/email_body.py
  31. +34
    -242
      webnotes/utils/email_lib/smtp.py

+ 40
- 0
config.json Voir le fichier

@@ -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"
}
}
}

+ 1
- 0
requirements.txt Voir le fichier

@@ -18,4 +18,5 @@ slugify
termcolor termcolor
werkzeug werkzeug
semantic_version semantic_version
lxml
inlinestyler inlinestyler

+ 7
- 0
webnotes/__init__.py Voir le fichier

@@ -220,6 +220,13 @@ def set_user(username):
def get_request_header(key, default=None): def get_request_header(key, default=None):
return request.headers.get(key, default) 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 logger = None
whitelisted = [] whitelisted = []
guest_methods = [] guest_methods = []


+ 9
- 36
webnotes/core/doctype/communication/communication.py Voir le fichier

@@ -3,6 +3,13 @@


from __future__ import unicode_literals from __future__ import unicode_literals
import webnotes 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(): class DocType():
def __init__(self, doc, doclist=[]): 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, # since we are using fullname and email,
# if the fullname has any incompatible characters,formataddr can deal with it # if the fullname has any incompatible characters,formataddr can deal with it
try: try:
import json
sender = json.loads(sender) sender = json.loads(sender)
except ValueError: except ValueError:
pass pass
if isinstance(sender, (tuple, list)) and len(sender)==2: if isinstance(sender, (tuple, list)) and len(sender)==2:
from email.utils import formataddr
sender = formataddr(sender) sender = formataddr(sender)
comm = webnotes.new_bean('Communication') 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 Get Customer/Supplier, given a contact, if a unique match exists
""" """
import webnotes
if not args: args = webnotes.local.form_dict if not args: args = webnotes.local.form_dict
if not args.get('contact'): if not args.get('contact'):
raise Exception, "Please specify a contact to fetch Customer/Supplier" raise Exception, "Please specify a contact to fetch Customer/Supplier"
@@ -92,7 +96,6 @@ def get_customer_supplier(args=None):
return {} return {}


def send_comm_email(d, name, sent_via=None, print_html=None, attachments='[]', send_me_a_copy=False): def send_comm_email(d, name, sent_via=None, print_html=None, attachments='[]', send_me_a_copy=False):
from json import loads
footer = None footer = None


if sent_via: 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) 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, mail = get_email(d.recipients, sender=d.sender, subject=d.subject,
msg=d.content, footer=footer) 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: if print_html:
mail.add_attachment(name.replace(' ','').replace('/','-') + '.html', print_html) mail.add_attachment(name.replace(' ','').replace('/','-') + '.html', print_html)


for a in loads(attachments):
for a in json.loads(attachments):
try: try:
mail.attach_file(a) mail.attach_file(a)
except IOError, e: except IOError, e:
webnotes.msgprint("""Unable to find attachment %s. Please resend without attaching this file.""" % a, webnotes.msgprint("""Unable to find attachment %s. Please resend without attaching this file.""" % a,
raise_exception=True) raise_exception=True)
mail.send()
send(mail)
def set_portal_link(sent_via, comm): def set_portal_link(sent_via, comm):
"""set portal link in footer""" """set portal link in footer"""
from webnotes.webutils import is_signup_enabled
from webnotes.utils import get_url, cstr
import urllib


footer = None 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 <a href="%s" target="_blank">View this on our website</a>""" % url
return footer 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})

+ 13
- 7
webnotes/core/doctype/print_format/print_format.py Voir le fichier

@@ -7,6 +7,8 @@ from webnotes import conf
import webnotes.utils import webnotes.utils
from webnotes.modules import get_doc_path from webnotes.modules import get_doc_path


standard_format = "templates/print_formats/standard.html"

class DocType: class DocType:
def __init__(self, d, dl): def __init__(self, d, dl):
self.doc, self.doclist = d,dl self.doc, self.doclist = d,dl
@@ -39,8 +41,9 @@ class DocType:
webnotes.clear_cache(doctype=self.doc.doc_type) webnotes.clear_cache(doctype=self.doc.doc_type)


def get_args(): 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 { return {
"body": """<h1>Error</h1> "body": """<h1>Error</h1>
<p>Parameters doctype, name and format required</p> <p>Parameters doctype, name and format required</p>
@@ -61,12 +64,12 @@ def get_args():
"comment": webnotes.session.user "comment": webnotes.session.user
} }


def get_html(doc, doclist):
def get_html(doc, doclist, print_format=None):
from jinja2 import Environment from jinja2 import Environment
from webnotes.core.doctype.print_format.print_format import get_print_format from webnotes.core.doctype.print_format.print_format import get_print_format


template = Environment().from_string(get_print_format(doc.doctype, 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) doctype = webnotes.get_doctype(doc.doctype)
args = { args = {
@@ -79,15 +82,18 @@ def get_html(doc, doclist):
html = template.render(args) html = template.render(args)
return html 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 # server, find template
path = os.path.join(get_doc_path(webnotes.conn.get_value("DocType", doctype, "module"), 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): if os.path.exists(path):
with open(path, "r") as pffile: with open(path, "r") as pffile:
return pffile.read() return pffile.read()
else: else:
html = webnotes.conn.get_value("Print Format", format, "html")
html = webnotes.conn.get_value("Print Format", format_name, "html")
if html: if html:
return html return html
else: else:


+ 1
- 1
webnotes/core/doctype/profile/profile.js Voir le fichier

@@ -34,7 +34,7 @@ cur_frm.cscript.user_image = function(doc) {
} }


cur_frm.cscript.refresh = 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..."); msgprint("Refreshing...");
window.location.reload(); window.location.reload();
} }


+ 8
- 59
webnotes/core/doctype/profile/profile.py Voir le fichier

@@ -2,7 +2,7 @@
# MIT License. See license.txt # MIT License. See license.txt


from __future__ import unicode_literals from __future__ import unicode_literals
import webnotes, json
import webnotes, json, os
from webnotes.utils import cint, now, cstr from webnotes.utils import cint, now, cstr
from webnotes import throw, msgprint, _ from webnotes import throw, msgprint, _
from webnotes.auth import _update_password from webnotes.auth import _update_password
@@ -127,73 +127,21 @@ class DocType:
(self.doc.first_name and " " or '') + (self.doc.last_name or '') (self.doc.first_name and " " or '') + (self.doc.last_name or '')


def password_reset_mail(self, link): 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): 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): def send_welcome_mail(self):
"""send welcome mail to user with password and login url"""

from webnotes.utils import random_string, get_url from webnotes.utils import random_string, get_url


self.doc.reset_password_key = random_string(32) self.doc.reset_password_key = random_string(32)
link = get_url("/update-password?key=" + self.doc.reset_password_key) 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""" """send mail with login details"""
import os
from webnotes.utils.email_lib import sendmail_md
from webnotes.profile import get_user_fullname from webnotes.profile import get_user_fullname
from webnotes.utils import get_url 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 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): def a_system_manager_should_exist(self):
if not self.get_other_system_managers(): if not self.get_other_system_managers():


+ 10
- 20
webnotes/core/page/messages/messages.py Voir le fichier

@@ -89,23 +89,13 @@ def delete(arg=None):
def notify(arg=None): def notify(arg=None):
from webnotes.utils import cstr, get_fullname, get_url 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()
})
)

+ 2
- 0
webnotes/patches.txt Voir le fichier

@@ -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', 'doctype', force=True) #2014-01-24
execute:webnotes.reload_doc('core', 'doctype', 'docfield', force=True) #2013-13-26 execute:webnotes.reload_doc('core', 'doctype', 'docfield', force=True) #2013-13-26
execute:webnotes.reload_doc('core', 'doctype', 'docperm') #2013-13-26 execute:webnotes.reload_doc('core', 'doctype', 'docperm') #2013-13-26


+ 80
- 75
webnotes/public/css/bootstrap.css Voir le fichier

@@ -1,10 +1,18 @@
/*! /*!
* Bootstrap v3.0.3 (http://getbootstrap.com)
* Bootstrap v3.1.0 (http://getbootstrap.com)
* Copyright 2011-2014 Twitter, Inc. * Copyright 2011-2014 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * 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, article,
aside, aside,
details, details,
@@ -21,8 +29,10 @@ summary {
} }
audio, audio,
canvas, canvas,
progress,
video { video {
display: inline-block; display: inline-block;
vertical-align: baseline;
} }
audio:not([controls]) { audio:not([controls]) {
display: none; display: none;
@@ -32,29 +42,13 @@ audio:not([controls]) {
template { template {
display: none; display: none;
} }
html {
font-family: sans-serif;

-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
}
a { a {
background: transparent; background: transparent;
} }
a:focus {
outline: thin dotted;
}
a:active, a:active,
a:hover { a:hover {
outline: 0; outline: 0;
} }
h1 {
margin: .67em 0;
font-size: 2em;
}
abbr[title] { abbr[title] {
border-bottom: 1px dotted; border-bottom: 1px dotted;
} }
@@ -65,28 +59,14 @@ strong {
dfn { dfn {
font-style: italic; font-style: italic;
} }
hr {
height: 0;
-moz-box-sizing: content-box;
box-sizing: content-box;
h1 {
margin: .67em 0;
font-size: 2em;
} }
mark { mark {
color: #000; color: #000;
background: #ff0; 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 { small {
font-size: 80%; font-size: 80%;
} }
@@ -110,28 +90,34 @@ svg:not(:root) {
overflow: hidden; overflow: hidden;
} }
figure { 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, button,
input, input,
optgroup,
select, select,
textarea { textarea {
margin: 0; margin: 0;
font-family: inherit;
font-size: 100%;
font: inherit;
color: inherit;
} }
button,
input {
line-height: normal;
button {
overflow: visible;
} }
button, button,
select { select {
@@ -148,11 +134,23 @@ button[disabled],
html input[disabled] { html input[disabled] {
cursor: default; cursor: default;
} }
button::-moz-focus-inner,
input::-moz-focus-inner {
padding: 0;
border: 0;
}
input {
line-height: normal;
}
input[type="checkbox"], input[type="checkbox"],
input[type="radio"] { input[type="radio"] {
box-sizing: border-box; box-sizing: border-box;
padding: 0; padding: 0;
} }
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
height: auto;
}
input[type="search"] { input[type="search"] {
-webkit-box-sizing: content-box; -webkit-box-sizing: content-box;
-moz-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 { input[type="search"]::-webkit-search-decoration {
-webkit-appearance: none; -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; padding: 0;
border: 0; border: 0;
} }
textarea { textarea {
overflow: auto; overflow: auto;
vertical-align: top;
}
optgroup {
font-weight: bold;
} }
table { table {
border-spacing: 0; border-spacing: 0;
border-collapse: collapse; border-collapse: collapse;
} }
td,
th {
padding: 0;
}
@media print { @media print {
* { * {
color: #000 !important; color: #000 !important;
@@ -213,9 +221,6 @@ table {
img { img {
max-width: 100% !important; max-width: 100% !important;
} }
@page {
margin: 2cm .5cm;
}
p, p,
h2, h2,
h3 { h3 {
@@ -296,6 +301,9 @@ a:focus {
outline: 5px auto -webkit-focus-ring-color; outline: 5px auto -webkit-focus-ring-color;
outline-offset: -2px; outline-offset: -2px;
} }
figure {
margin: 0;
}
img { img {
vertical-align: middle; vertical-align: middle;
} }
@@ -1620,6 +1628,7 @@ table th[class*="col-"] {
} }
} }
fieldset { fieldset {
min-width: 0;
padding: 0; padding: 0;
margin: 0; margin: 0;
border: 0; border: 0;
@@ -1662,11 +1671,6 @@ select[multiple],
select[size] { select[size] {
height: auto; height: auto;
} }
select optgroup {
font-family: inherit;
font-size: inherit;
font-style: inherit;
}
input[type="file"]:focus, input[type="file"]:focus,
input[type="radio"]:focus, input[type="radio"]:focus,
input[type="checkbox"]:focus { input[type="checkbox"]:focus {
@@ -1674,10 +1678,6 @@ input[type="checkbox"]:focus {
outline: 5px auto -webkit-focus-ring-color; outline: 5px auto -webkit-focus-ring-color;
outline-offset: -2px; outline-offset: -2px;
} }
input[type="number"]::-webkit-outer-spin-button,
input[type="number"]::-webkit-inner-spin-button {
height: auto;
}
output { output {
display: block; display: block;
padding-top: 7px; padding-top: 7px;
@@ -1699,7 +1699,7 @@ output {
border-radius: 4px; border-radius: 4px;
} }
.form-control:focus { .form-control:focus {
border-color: #66afe9;
border-color: #000;
outline: 0; outline: 0;
} }
.form-control:-moz-placeholder { .form-control:-moz-placeholder {
@@ -1720,6 +1720,7 @@ output {
fieldset[disabled] .form-control { fieldset[disabled] .form-control {
cursor: not-allowed; cursor: not-allowed;
background-color: #eee; background-color: #eee;
opacity: 1;
} }
textarea.form-control { textarea.form-control {
height: auto; height: auto;
@@ -1794,7 +1795,8 @@ select.input-sm {
height: 30px; height: 30px;
line-height: 30px; line-height: 30px;
} }
textarea.input-sm {
textarea.input-sm,
select[multiple].input-sm {
height: auto; height: auto;
} }
.input-lg { .input-lg {
@@ -1808,7 +1810,8 @@ select.input-lg {
height: 46px; height: 46px;
line-height: 46px; line-height: 46px;
} }
textarea.input-lg {
textarea.input-lg,
select[multiple].input-lg {
height: auto; height: auto;
} }
.has-feedback { .has-feedback {
@@ -3312,7 +3315,10 @@ select.input-group-lg > .input-group-btn > .btn {
} }
textarea.input-group-lg > .form-control, textarea.input-group-lg > .form-control,
textarea.input-group-lg > .input-group-addon, 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; height: auto;
} }
.input-group-sm > .form-control, .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 > .form-control,
textarea.input-group-sm > .input-group-addon, 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; height: auto;
} }
.input-group-addon, .input-group-addon,
@@ -3717,6 +3726,7 @@ textarea.input-group-sm > .input-group-btn > .btn {
} }
.navbar-brand { .navbar-brand {
float: left; float: left;
height: 20px;
padding: 8px 15px; padding: 8px 15px;
font-size: 18px; font-size: 18px;
line-height: 20px; line-height: 20px;
@@ -3725,11 +3735,6 @@ textarea.input-group-sm > .input-group-btn > .btn {
.navbar-brand:focus { .navbar-brand:focus {
text-decoration: none; text-decoration: none;
} }
.navbar-brand > .glyphicon {
float: left;
margin-top: -2px;
margin-right: 5px;
}
@media (min-width: 768px) { @media (min-width: 768px) {
.navbar > .container .navbar-brand, .navbar > .container .navbar-brand,
.navbar > .container-fluid .navbar-brand { .navbar > .container-fluid .navbar-brand {
@@ -4834,8 +4839,8 @@ a.list-group-item-danger.active:focus {
.panel > .panel-body + .table-responsive { .panel > .panel-body + .table-responsive {
border-top: 1px solid #ddd; 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; border-top: 0;
} }
.panel > .table-bordered, .panel > .table-bordered,


+ 8
- 0
webnotes/public/css/font-awesome.css Voir le fichier

@@ -32,6 +32,14 @@
font-weight: normal; font-weight: normal;
font-style: 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 /* FONT AWESOME CORE
* -------------------------- */ * -------------------------- */
[class^="icon-"], [class^="icon-"],


+ 9
- 0
webnotes/public/css/less/webnotes.less Voir le fichier

@@ -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;
}

+ 1
- 1
webnotes/public/js/legacy/print_format.js Voir le fichier

@@ -389,7 +389,7 @@ $.extend(_p, {
lh = cstr(wn.boot.letter_heads[cur_frm.doc.letter_head]); lh = cstr(wn.boot.letter_heads[cur_frm.doc.letter_head]);
} else if (cp.letter_head) { } else if (cp.letter_head) {
lh = cp.letter_head; lh = cp.letter_head;
}
}
return lh; return lh;
}, },


+ 1
- 1
webnotes/public/js/lib/bootstrap.min.js Voir le fichier

@@ -1,5 +1,5 @@
/*! /*!
* Bootstrap v3.0.3 (http://getbootstrap.com)
* Bootstrap v3.1.0 (http://getbootstrap.com)
* Copyright 2011-2014 Twitter, Inc. * Copyright 2011-2014 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
*/ */

+ 8
- 7
webnotes/public/js/wn/model/meta.js Voir le fichier

@@ -100,13 +100,8 @@ $.extend(wn.meta, {
}, },
get_print_formats: function(doctype) { 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}) var print_formats = wn.model.get("Print Format", {doc_type: doctype})
.sort(function(a, b) { return (a > b) ? 1 : -1; }); .sort(function(a, b) { return (a > b) ? 1 : -1; });
@@ -114,6 +109,12 @@ $.extend(wn.meta, {
if(!in_list(print_format_list, d.name)) if(!in_list(print_format_list, d.name))
print_format_list.push(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; return print_format_list;
}, },


+ 1
- 1
webnotes/public/js/wn/ui/editor.js Voir le fichier

@@ -125,7 +125,7 @@ bsEditor = Class.extend({
clean_html: function() { clean_html: function() {
var html = this.editor.html() || ""; 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>|&nbsp;)*$/, ''); // html = html.replace(/(<br>|\s|<div><br><\/div>|&nbsp;)*$/, '');


// remove custom typography (use CSS!) // remove custom typography (use CSS!)


+ 0
- 0
webnotes/templates/emails/.txt Voir le fichier


+ 5
- 0
webnotes/templates/emails/new_message.html Voir le fichier

@@ -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>

+ 10
- 0
webnotes/templates/emails/new_user.html Voir le fichier

@@ -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>

+ 6
- 0
webnotes/templates/emails/password_reset.html Voir le fichier

@@ -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>

+ 5
- 0
webnotes/templates/emails/password_update.html Voir le fichier

@@ -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>

webnotes/templates/emails/html_email_template.html → webnotes/templates/emails/standard.html Voir le fichier

@@ -25,9 +25,9 @@ body {
-webkit-text-size-adjust:none; -webkit-text-size-adjust:none;
width: 100%!important; width: 100%!important;
height: 100%; height: 100%;
background-color: #f6f6f6;
} }



/* ------------------------------------- /* -------------------------------------
ELEMENTS ELEMENTS
------------------------------------- */ ------------------------------------- */
@@ -115,7 +115,7 @@ table.footer-wrap a{
------------------------------------- */ ------------------------------------- */
h1,h2,h3{ h1,h2,h3{
font-family: "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; line-height: 1.1; margin-bottom:15px; color:#000; 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; line-height: 1.2;
font-weight:200; font-weight:200;
} }
@@ -129,7 +129,10 @@ h2 {
h3 { h3 {
font-size: 22px; font-size: 22px;
} }

hr {
margin: 20px 0;
border-top: 1px solid #eee;
}
p, ul, ol { p, ul, ol {
margin-bottom: 10px; margin-bottom: 10px;
font-weight: normal; font-weight: normal;

+ 0
- 0
webnotes/templates/print_formats/standard.html Voir le fichier


+ 1
- 1
webnotes/test_runner.py Voir le fichier

@@ -30,7 +30,7 @@ def main(app=None, module=None, doctype=None, verbose=False):
for doctype in module.test_dependencies: for doctype in module.test_dependencies:
make_test_records(doctype, verbose=verbose) 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) ret = unittest.TextTestRunner(verbosity=1+(verbose and 1 or 0)).run(test_suite)
else: else:
ret = run_all_tests(app, verbose) ret = run_all_tests(app, verbose)


+ 0
- 4
webnotes/tests/test_email.py Voir le fichier

@@ -4,10 +4,6 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import os, sys import os, sys


sys.path.append('.')
sys.path.append('lib/py')
sys.path.append('erpnext')

import unittest, webnotes import unittest, webnotes


from webnotes.test_runner import make_test_records from webnotes.test_runner import make_test_records


+ 1
- 1
webnotes/translate.py Voir le fichier

@@ -288,4 +288,4 @@ def google_translate(lang, untranslated):
return dict(zip(untranslated, translated)) return dict(zip(untranslated, translated))
else: else:
print "unable to translate" print "unable to translate"
return {}
return {}

+ 22
- 12
webnotes/utils/__init__.py Voir le fichier

@@ -5,9 +5,11 @@


from __future__ import unicode_literals from __future__ import unicode_literals
from werkzeug.test import Client from werkzeug.test import Client
import os
import re
import urllib


import webnotes import webnotes
import os


no_value_fields = ['Section Break', 'Column Break', 'HTML', 'Table', 'FlexTable', no_value_fields = ['Section Break', 'Column Break', 'HTML', 'Table', 'FlexTable',
'Button', 'Image', 'Graph'] 'Button', 'Image', 'Graph']
@@ -64,7 +66,6 @@ def extract_email_id(email):
def validate_email_add(email_str): def validate_email_add(email_str):
"""Validates the email string""" """Validates the email string"""
email = extract_email_id(email_str) 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()) 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): def get_request_site_address(full_address=False):
@@ -277,7 +278,6 @@ def dict_to_str(args, sep='&'):
""" """
Converts a dictionary to URL Converts a dictionary to URL
""" """
import urllib
t = [] t = []
for k in args.keys(): for k in args.keys():
t.append(str(k)+'='+urllib.quote(str(args[k] or ''))) t.append(str(k)+'='+urllib.quote(str(args[k] or '')))
@@ -670,7 +670,6 @@ def strip_html(text):
""" """
removes anything enclosed in and including <> removes anything enclosed in and including <>
""" """
import re
return re.compile(r'<.*?>').sub('', text) return re.compile(r'<.*?>').sub('', text)
def escape_html(text): def escape_html(text):
@@ -831,7 +830,6 @@ def get_url(uri=None):
url = "http://" + subdomain url = "http://" + subdomain
if uri: if uri:
import urllib
url = urllib.basejoin(url, uri) url = urllib.basejoin(url, uri)
return url return url
@@ -896,14 +894,26 @@ def get_disk_usage():
return 0 return 0
err, out = execute_in_shell("du -hsm {files_path}".format(files_path=files_path)) err, out = execute_in_shell("du -hsm {files_path}".format(files_path=files_path))
return cint(out.split("\n")[-2].split("\t")[0]) 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() url = get_url()
if not url.endswith("/"): 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): def touch_file(path):
with open(path, 'a'): with open(path, 'a'):
@@ -912,4 +922,4 @@ def touch_file(path):


def get_test_client(): def get_test_client():
from webnotes.app import application from webnotes.app import application
return Client(application)
return Client(application)

+ 6
- 4
webnotes/utils/email_lib/__init__.py Voir le fichier

@@ -5,6 +5,9 @@ from __future__ import unicode_literals
import webnotes import webnotes
from webnotes import conf 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): def sendmail_md(recipients, sender=None, msg=None, subject=None):
"""send markdown email""" """send markdown email"""
import markdown2 import markdown2
@@ -12,12 +15,11 @@ def sendmail_md(recipients, sender=None, msg=None, subject=None):
def sendmail(recipients, sender='', msg='', subject='[No Subject]'): def sendmail(recipients, sender='', msg='', subject='[No Subject]'):
"""send an html email as multipart with attachments and all""" """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): 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() @webnotes.whitelist()
def get_contact_list(): def get_contact_list():


+ 25
- 32
webnotes/utils/email_lib/bulk.py Voir le fichier

@@ -3,31 +3,29 @@


from __future__ import unicode_literals from __future__ import unicode_literals
import webnotes import webnotes
from webnotes import msgprint, throw, _
from webnotes.model.doc import Document
from webnotes.utils import cint, get_url
import HTMLParser
import urllib 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 class BulkLimitCrossedError(webnotes.ValidationError): pass


def send(recipients=None, sender=None, doctype='Profile', email_field='email', def send(recipients=None, sender=None, doctype='Profile', email_field='email',
subject='[No Subject]', message='[No Content]', ref_doctype=None, ref_docname=None, subject='[No Subject]', message='[No Content]', ref_doctype=None, ref_docname=None,
add_unsubscribe_link=True): add_unsubscribe_link=True):
"""send bulk mail if not unsubscribed and within conf.bulk_mail_limit"""
import webnotes
def is_unsubscribed(rdata): def is_unsubscribed(rdata):
if not rdata: return 1
if not rdata:
return 1
return cint(rdata.unsubscribed) return cint(rdata.unsubscribed)


def check_bulk_limit(new_mails): 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 this_month = webnotes.conn.sql("""select count(*) from `tabBulk Email` where
month(creation)=month(%s)""" % nowdate())[0][0] 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: if this_month + len(recipients) > monthly_bulk_mail_limit:
throw("{bulk} ({limit}) {cross}".format(**{ throw("{bulk} ({limit}) {cross}".format(**{
@@ -36,10 +34,10 @@ def send(recipients=None, sender=None, doctype='Profile', email_field='email',
"cross": _("crossed") "cross": _("crossed")
}), exc=BulkLimitCrossedError) }), exc=BulkLimitCrossedError)


def update_message(doc):
updated = message
def update_message(formatted, doc, add_unsubscribe_link):
updated = formatted
if add_unsubscribe_link: 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;"> margin-top: 17px;">
<small><a href="%s/?%s"> <small><a href="%s/?%s">
Unsubscribe</a> from this list.</small></div>""" % (get_url(), 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, "type": doctype,
"email_field": email_field "email_field": email_field
})) }))
updated = updated.replace("<!--unsubscribe link here-->", unsubscribe_link)
return updated return updated
@@ -56,36 +56,33 @@ def send(recipients=None, sender=None, doctype='Profile', email_field='email',
if not sender or sender == "Administrator": if not sender or sender == "Administrator":
sender = webnotes.conn.get_value('Email Settings', None, 'auto_email_id') sender = webnotes.conn.get_value('Email Settings', None, 'auto_email_id')
check_bulk_limit(len(recipients)) check_bulk_limit(len(recipients))

import HTMLParser
from webnotes.utils.email_lib.html2text import html2text
from webnotes.utils import expand_partial_links
try: try:
message = expand_partial_links(message)
text_content = html2text(message) text_content = html2text(message)
except HTMLParser.HTMLParseError: except HTMLParser.HTMLParseError:
text_content = "[See html attachment]" text_content = "[See html attachment]"
formatted = get_formatted_html(subject, message)

for r in filter(None, list(set(recipients))): for r in filter(None, list(set(recipients))):
rdata = webnotes.conn.sql("""select * from `tab%s` where %s=%s""" % (doctype, rdata = webnotes.conn.sql("""select * from `tab%s` where %s=%s""" % (doctype,
email_field, '%s'), (r,), as_dict=1) email_field, '%s'), (r,), as_dict=1)


doc = rdata and rdata[0] or {} doc = rdata and rdata[0] or {}
if not is_unsubscribed(doc): if not is_unsubscribed(doc):
# add to queue # 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.sender = sender
e.recipient = email e.recipient = email
try: 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() text_content = text_content).as_string()
except webnotes.ValidationError: except webnotes.ValidationError:
# bad email id - don't add to queue # bad email id - don't add to queue
@@ -116,15 +113,11 @@ def unsubscribe():
def flush(from_test=False): def flush(from_test=False):
"""flush email queue, every time: called from scheduler""" """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() smptserver = SMTPServer()
auto_commit = not from_test 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")) msgprint(_("Emails are muted"))
from_test = True from_test = True


+ 217
- 0
webnotes/utils/email_lib/email_body.py Voir le fichier

@@ -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

+ 34
- 242
webnotes/utils/email_lib/smtp.py Voir le fichier

@@ -2,247 +2,43 @@
# MIT License. See license.txt # MIT License. See license.txt


from __future__ import unicode_literals from __future__ import unicode_literals
"""
Sends email via outgoing server specified in "Control Panel"
Allows easy adding of Attachments of "File" objects
"""


import webnotes 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: class SMTPServer:
def __init__(self, login=None, password=None, server=None, port=None, use_ssl=None): 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 # get defaults from control panel
try: try:
es = webnotes.model.doc.Document('Email Settings','Email Settings')
es = webnotes.doc('Email Settings','Email Settings')
except webnotes.DoesNotExistError: except webnotes.DoesNotExistError:
es = None es = None
@@ -261,11 +57,11 @@ class SMTPServer:
self.password = es.mail_password self.password = es.mail_password
self.always_use_login_id_as_sender = es.always_use_login_id_as_sender self.always_use_login_id_as_sender = es.always_use_login_id_as_sender
else: 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 @property
def sess(self): def sess(self):
@@ -273,10 +69,6 @@ class SMTPServer:
if self._sess: if self._sess:
return self._sess return self._sess
from webnotes.utils import cint
import smtplib
import _socket
# check if email server specified # check if email server specified
if not self.server: if not self.server:
err_msg = 'Outgoing Mail Server not specified' err_msg = 'Outgoing Mail Server not specified'
@@ -306,7 +98,7 @@ class SMTPServer:


# check if logged correctly # check if logged correctly
if ret[0]!=235: if ret[0]!=235:
msgprint(ret[1])
webnotes.msgprint(ret[1])
raise webnotes.OutgoingEmailError, ret[1] raise webnotes.OutgoingEmailError, ret[1]


return self._sess return self._sess


Chargement…
Annuler
Enregistrer