refactored email libraries and added bulk email capability

This commit is contained in:
Rushabh Mehta 2012-08-02 18:02:38 +05:30
parent 8598d18cf8
commit ef38508b3b
8 changed files with 397 additions and 186 deletions

View file

View file

@ -0,0 +1,103 @@
# DocType, Bulk Email
[
# These values are common in all dictionaries
{
'creation': '2012-08-02 15:17:28',
'docstatus': 0,
'modified': '2012-08-02 15:17:29',
'modified_by': u'Administrator',
'owner': u'Administrator'
},
# These values are common for all DocType
{
'description': u'Bulk Email records.',
'doctype': 'DocType',
'document_type': u'System',
'in_create': 1,
'module': u'Core',
'name': '__common__',
'read_only': 1,
'version': 1
},
# These values are common for all DocField
{
'doctype': u'DocField',
'name': '__common__',
'parent': u'Bulk Email',
'parentfield': u'fields',
'parenttype': u'DocType',
'permlevel': 0
},
# These values are common for all DocPerm
{
'doctype': u'DocPerm',
'name': '__common__',
'parent': u'Bulk Email',
'parentfield': u'permissions',
'parenttype': u'DocType',
'permlevel': 0,
'read': 1
},
# DocType, Bulk Email
{
'doctype': 'DocType',
'name': u'Bulk Email'
},
# DocPerm
{
'doctype': u'DocPerm',
'role': u'Administrator'
},
# DocPerm
{
'doctype': u'DocPerm',
'role': u'System Manager'
},
# DocField
{
'doctype': u'DocField',
'fieldname': u'sender',
'fieldtype': u'Data',
'label': u'Sender'
},
# DocField
{
'doctype': u'DocField',
'fieldname': u'recipient',
'fieldtype': u'Data',
'label': u'Recipient'
},
# DocField
{
'doctype': u'DocField',
'fieldname': u'message',
'fieldtype': u'Text',
'label': u'Message'
},
# DocField
{
'doctype': u'DocField',
'fieldname': u'status',
'fieldtype': u'Data',
'label': u'Status'
},
# DocField
{
'doctype': u'DocField',
'fieldname': u'error',
'fieldtype': u'Text',
'label': u'Error'
}
]

View file

@ -0,0 +1,65 @@
import os, sys
sys.path.append('.')
sys.path.append('lib/py')
sys.path.append('erpnext')
import unittest, webnotes
class TestEmail(unittest.TestCase):
def setUp(self):
webnotes.conn.begin()
def tearDown(self):
webnotes.conn.rollback()
def test_send(self):
from webnotes.utils.email_lib import sendmail
#sendmail('rmehta@gmail.com', subject='Test Mail', msg="Test Content")
def test_bulk(self):
from webnotes.utils.email_lib.bulk import send
send(recipients = ['rmehta@gmail.com', 'rushabh@erpnext.com'],
doctype='Lead', email_field='email_id', first_name_field='lead_name',
last_name_field=None, subject='Testing Bulk', message='This is a bulk mail!')
bulk = webnotes.conn.sql("""select * from `tabBulk Email` where status='Not Sent'""", as_dict=1)
self.assertEquals(len(bulk), 2)
self.assertTrue('rmehta@gmail.com' in [d['recipient'] for d in bulk])
self.assertTrue('rushabh@erpnext.com' in [d['recipient'] for d in bulk])
self.assertTrue('Unsubscribe' in bulk[0]['message'])
def test_flush(self):
self.test_bulk()
from webnotes.utils.email_lib.bulk import flush
flush()
bulk = webnotes.conn.sql("""select * from `tabBulk Email` where status='Sent'""", as_dict=1)
self.assertEquals(len(bulk), 2)
self.assertTrue('rmehta@gmail.com' in [d['recipient'] for d in bulk])
self.assertTrue('rushabh@erpnext.com' in [d['recipient'] for d in bulk])
webnotes.conn.sql("""delete from `tabBulk Email`""", auto_commit=True)
def test_unsubscribe(self):
from webnotes.utils.email_lib.bulk import unsubscribe, send
webnotes.form_dict = {
'email':'rmehta@gmail.com',
'type':'Lead',
'email_field':'email_id'
}
unsubscribe()
send(recipients = ['rmehta@gmail.com', 'rushabh@erpnext.com'],
doctype='Lead', email_field='email_id', first_name_field='lead_name',
last_name_field=None, subject='Testing Bulk', message='This is a bulk mail!')
bulk = webnotes.conn.sql("""select * from `tabBulk Email` where status='Not Sent'""",
as_dict=1)
self.assertEquals(len(bulk), 1)
self.assertFalse('rmehta@gmail.com' in [d['recipient'] for d in bulk])
self.assertTrue('rushabh@erpnext.com' in [d['recipient'] for d in bulk])
self.assertTrue('Unsubscribe' in bulk[0]['message'])
if __name__=='__main__':
webnotes.connect()
unittest.main()

View file

@ -66,21 +66,26 @@ def extract_email_id(s):
return s
def validate_email_add(email_str):
"""
Validates the email string
"""
"""Validates the email string"""
s = extract_email_id(email_str)
import re
#return re.match("^[a-zA-Z0-9._%-]+@[a-zA-Z0-9._%-]+.[a-zA-Z]{2,6}$", email_str)
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])?", s)
def sendmail(recipients, sender='', msg='', subject='[No Subject]', parts=[], cc=[], attach=[]):
"""
Send an email. For more details see :func:`email_lib.sendmail`
"""
"""Send an email. For more details see :func:`email_lib.sendmail`"""
import webnotes.utils.email_lib
return email_lib.sendmail(recipients, sender, msg, subject, parts, cc, attach)
def get_request_site_address():
"""get app url from request"""
import os
try:
return 'HTTPS' in os.environ.get('SERVER_PROTOCOL') and 'https://' or 'http://' \
+ os.environ.get('HTTP_HOST')
except TypeError, e:
return 'http://localhost'
def generate_hash():
"""
Generates random hash for session id

View file

@ -22,101 +22,25 @@
import webnotes
def sendmail_html(sender, recipients, subject, html, text=None, template=None, send_now=1, reply_to=None):
"""
Send an html mail with alternative text and using Page Templates
"""
sendmail(recipients, sender, html, subject,
send_now = send_now, reply_to = reply_to, template = template)
def make_html_body(content, template = None):
"""
Generate html content from a Page Template object
"""
template_html = '<div class="margin: 12px">%(content)s</div>'
if template:
from webnotes.model.code import get_code
template_html = get_code(webnotes.conn.get_value('Page Template', template, 'module'), 'Page Template', template, 'html', fieldname='template')
return template_html % {'content': content}
def sendmail_md(recipients, sender=None, msg=None, subject=None, from_defs=0):
def sendmail_md(recipients, sender=None, msg=None, subject=None):
"""send markdown email"""
import markdown2
sendmail(recipients, sender, markdown2.markdown(msg), subject, txt=msg, from_defs=from_defs)
def sendmail(recipients, sender='', msg='', subject='[No Subject]', txt=None, \
parts=[], cc=[], attach=[], send_now=1, reply_to=None, template=None, from_defs=0):
"""
send an html email as multipart with attachments and all
"""
from webnotes.utils.email_lib.html2text import html2text
from webnotes.utils.email_lib.send import EMail
import HTMLParser
email = EMail(sender, recipients, subject, reply_to=reply_to, from_defs=from_defs)
email.cc = cc
if msg:
if template:
msg = make_html_body(msg, template)
else:
# if not html, then lets put some whitespace
if (not '<br>' in msg) and (not '<p>' in msg):
msg = msg.replace('\n','<br>')
footer = get_footer()
# encode using utf-8
footer = footer.encode('utf-8', 'ignore')
msg = msg + (footer or '')
if txt:
email.set_text(txt)
else:
try:
msg_unicode = msg
if isinstance(msg, str):
msg_unicode = unicode(msg, 'utf-8', 'ignore')
email.set_text(html2text(msg_unicode))
except HTMLParser.HTMLParseError:
pass
email.set_html(msg)
for p in parts:
email.set_message(p[1])
for a in attach:
email.attach(a)
email.send(send_now)
def get_footer():
"""
Returns combination of footer from globals and Control Panel
"""
footer = webnotes.conn.get_value('Control Panel',None,'mail_footer') or ''
footer += (webnotes.conn.get_global('global_mail_footer') or '')
return footer
sendmail(recipients, sender, markdown2.markdown(msg), subject)
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()
@webnotes.whitelist()
def send_form():
"""
Emails a print format (form)
Called from form UI
"""
"""Emails a print format (form). Called from form UI"""
from webnotes.utils.email_lib.form_email import FormEmail
FormEmail().send()
@webnotes.whitelist()
def get_contact_list():
"""
Returns contacts (from autosuggest)
"""
"""Returns contacts (from autosuggest)"""
cond = ['`%s` like "%s%%"' % (f,
webnotes.form.getvalue('txt')) for f in webnotes.form.getvalue('where').split(',')]
cl = webnotes.conn.sql("select `%s` from `tab%s` where %s" % (
@ -126,5 +50,3 @@ def get_contact_list():
)
)
webnotes.response['cl'] = filter(None, [c[0] for c in cl])

View file

@ -0,0 +1,118 @@
# Copyright (c) 2012 Web Notes Technologies Pvt Ltd (http://erpnext.com)
#
# MIT License (MIT)
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
# INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
# PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
# CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
# OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
import webnotes
def send(recipients=[], doctype='Profile', email_field='email', first_name_field="first_name",
last_name_field="last_name", subject='[No Subject]', message='[No Content]'):
"""send bulk mail if not unsubscribed and within conf.bulk_mail_limit"""
import webnotes
def is_unsubscribed(rdata):
if not rdata: return 1
return rdata[0]['unsubscribed']
def check_bulk_limit(new_mails):
import conf
from webnotes.utils import nowdate
todays_bulk = webnotes.conn.sql("""select count(*) from `tabBulk Email` where
datediff(%s, creation)<1""" % nowdate())[0][0]
bulk_mail_limit = getattr(conf, 'bulk_mail_limit', 200)
if todays_bulk + len(recipients) > bulk_mail_limit:
webnotes.msgprint("""Buik Mail Limit Crossed""", raise_exception=1)
def add_unsubscribe_link(email):
from webnotes.utils import get_request_site_address
return message + """<div style="padding: 7px; border-top: 1px solid #aaa>">
<small><a href="http://%s/server.py?cmd=%s&email=%s&type=%s&email_field=%s">
Unsubscribe</a> from this list.</small></div>""" % (get_request_site_address(),
'webnotes.utils.email_lib.bulk.unsubscribe', email, doctype, email_field)
def full_name(rdata):
fname = rdata[0].get(first_name_field, '')
lname = rdata[0].get(last_name_field, '')
if fname and not lname:
return fname
elif lname and not fname:
return lname
elif fname and lname:
return fname + ' ' + lname
else:
return rdata[0][email_field].split('@')[0].title()
check_bulk_limit(len(recipients))
sender = webnotes.conn.get_value('Email Settings', None, 'auto_mail_id')
for r in recipients:
rdata = webnotes.conn.sql("""select * from `tab%s` where %s=%s""" % (doctype,
email_field, '%s'), r, as_dict=1)
if not is_unsubscribed(rdata):
# add to queue
add(r, sender, subject, add_unsubscribe_link(r) % {"full_name":full_name(rdata)})
def add(email, sender, subject, message):
"""add to bulk mail queue"""
from webnotes.model.doc import Document
from webnotes.utils.email_lib.smtp import get_email
e = Document('Bulk Email')
e.sender = sender
e.recipient = email
e.message = get_email(email, sender=e.sender, msg=message, subject=subject).as_string()
e.status = 'Not Sent'
e.save()
@webnotes.whitelist(allow_guest=True)
def unsubscribe():
doctype = webnotes.form_dict.get('type')
field = webnotes.form_dict.get('email_field')
email = webnotes.form_dict.get('email')
webnotes.conn.sql("""update `tab%s` set unsubscribed=1
where email_id=%s""" % (doctype, '%s'), email)
webnotes.unsubscribed_email = email
webnotes.response['type'] = 'page'
webnotes.response['page_name'] = 'unsubscribed.html'
def flush():
"""flush email queue, every time: called from scheduler"""
import webnotes
from webnotes.utils.email_lib.smtp import SMTPServer
smptserver = SMTPServer()
for email in webnotes.conn.sql("""select * from `tabBulk Email` where status='Not Sent'""",
as_dict=1):
webnotes.conn.sql("""update `tabBulk Email` set status='Sending' where name=%s""",
email["name"], auto_commit=True)
try:
smptserver.sess.sendmail(email["sender"], email["recipient"], email["message"])
webnotes.conn.sql("""update `tabBulk Email` set status='Sent' where name=%s""",
email["name"], auto_commit=True)
except Exception, e:
webnotes.conn.sql("""update `tabBulk Email` set status='Error', error=%s
where name=%s""", (str(e), email["name"]), auto_commit=True)
def clear_outbox():
"""remove mails older than 7 days in Outbox"""
webnotes.conn.sql("""delete from `tabBulk Email` where
datediff(now(), creation) > 7""", (str(e), email["name"]), auto_commit=True)

View file

@ -26,7 +26,7 @@ from webnotes.utils import cint
form = webnotes.form
from webnotes.utils.email_lib import get_footer
from webnotes.utils.email_lib.send import EMail
from webnotes.utils.email_lib.smtp import EMail
class FormEmail:
"""
@ -204,7 +204,7 @@ class FormEmail:
if self.cc:
self.email.cc = [self.cc]
self.email.send(send_now=1)
self.email.send()
self.make_communication()
webnotes.msgprint('Sent')

View file

@ -30,13 +30,22 @@ import conf
from webnotes import msgprint
import email
def get_email(recipients, sender='', msg='', subject='[No Subject]'):
"""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)
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='', from_defs=0, alternative=0, reply_to=None):
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')
@ -48,7 +57,6 @@ class EMail:
# remove null
recipients = filter(None, recipients)
self.from_defs = from_defs
self.sender = sender
self.reply_to = reply_to or sender
self.recipients = recipients
@ -58,6 +66,7 @@ class EMail:
self.msg_multipart = MIMEMultipart('alternative')
self.msg_root.attach(self.msg_multipart)
self.cc = []
self.html_set = False
def set_text(self, message):
"""
@ -68,21 +77,32 @@ class EMail:
message = message.encode('utf-8')
part = MIMEText(message, 'plain', 'utf-8')
self.msg_multipart.attach(part)
def set_html(self, message):
"""
Attach message in the html portion of multipart/alternative
"""
from email.mime.text import MIMEText
if isinstance(message, unicode):
message = message.encode('utf-8')
part = MIMEText(message, 'html', 'utf-8')
"""Attach message in the html portion of multipart/alternative"""
from email.mime.text import MIMEText
message = unicode(message) + self.get_footer()
# this is the first html part of a multi-part message,
# convert to text well
if not self.html_set:
self.set_html_text(message)
part = MIMEText(message.encode('utf-8'), 'html', 'utf-8')
self.msg_multipart.attach(part)
self.html_set = True
def set_html_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)
"""
"""Append the message with MIME content to the root node (as attachment)"""
from email.mime.text import MIMEText
maintype, subtype = mime_type.split('/')
@ -92,11 +112,15 @@ class EMail:
part.add_header('Content-Disposition', 'attachment', filename=filename)
self.msg_root.attach(part)
def get_footer(self):
"""append a footer"""
footer = webnotes.conn.get_value('Control Panel',None,'mail_footer') or ''
footer += (webnotes.conn.get_global('global_mail_footer') or '')
return unicode(footer)
def attach_file(self, n):
"""
attach a file from the `FileData` table
"""
"""attach a file from the `FileData` table"""
from webnotes.utils.file_manager import get_file
res = get_file(n)
if not res:
@ -105,7 +129,7 @@ class EMail:
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
@ -143,12 +167,10 @@ class EMail:
self.msg_root.attach(part)
def validate(self):
"""
validate the email ids
"""
"""validate the email ids"""
if not self.sender:
self.sender = hasattr(conf, 'auto_email_id') \
and conf.auto_email_id or '"ERPNext Notification" <automail@erpnext.com>'
self.sender = webnotes.conn.get_value('Email Settings', None, 'auto_email_id') \
or getattr(conf, 'auto_email_id', '"ERPNext Notification" <notification@erpnext.com>')
from webnotes.utils import validate_email_add
# validate ids
@ -162,31 +184,8 @@ class EMail:
if not validate_email_add(e):
webnotes.msgprint("%s is not a valid email id" % e, raise_exception = 1)
def setup(self):
"""
setup the SMTP (outgoing) server from `Control Panel` or defs.py
"""
if self.from_defs:
import webnotes
self.server = getattr(conf,'mail_server','')
self.login = getattr(conf,'mail_login','')
self.port = getattr(conf,'mail_port',None)
self.password = getattr(conf,'mail_password','')
self.use_ssl = getattr(conf,'use_ssl',0)
else:
import webnotes.model.doc
from webnotes.utils import cint
# get defaults from control panel
es = webnotes.model.doc.Document('Email Settings','Email Settings')
self.server = es.outgoing_mail_server.encode('utf-8') or getattr(conf,'mail_server','')
self.login = es.mail_login.encode('utf-8') or getattr(conf,'mail_login','')
self.port = cint(es.mail_port) or getattr(conf,'mail_port',None)
self.password = es.mail_password.encode('utf-8') or getattr(conf,'mail_password','')
self.use_ssl = cint(es.use_ssl) or cint(getattr(conf, 'use_ssl', ''))
def make_msg(self):
def make(self):
"""build into msg_root"""
self.msg_root['Subject'] = self.subject
self.msg_root['From'] = self.sender
self.msg_root['To'] = ', '.join([r.strip() for r in self.recipients])
@ -194,46 +193,43 @@ class EMail:
self.msg_root['Reply-To'] = self.reply_to
if self.cc:
self.msg_root['CC'] = ', '.join([r.strip() for r in self.cc])
def add_to_queue(self):
# write to a file called "email_queue" or as specified in email
q = EmailQueue()
q.push({
'server': self.server,
'port': self.port,
'use_ssl': self.use_ssl,
'login': self.login,
'password': self.password,
'sender': self.sender,
'recipients': self.recipients,
'msg': self.msg_root.as_string()
})
q.close()
def send(self, send_now = 0):
"""
send the message
"""
from webnotes.utils import cint
self.setup()
def as_string(self):
"""validate, build message and convert to string"""
self.validate()
self.make_msg()
self.make()
return self.msg_root.as_string()
sess = self.smtp_connect()
def send(self, as_bulk=False):
"""send the message or add it to Outbox Email"""
SMTPServer().sess.sendmail(self.sender, self.recipients, self.as_string())
sess.sendmail(self.sender, self.recipients, self.msg_root.as_string())
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
es = webnotes.model.doc.Document('Email Settings','Email Settings')
self.server = server or es.outgoing_mail_server.encode('utf-8') \
or getattr(conf,'mail_server','')
self.login = login or es.mail_login.encode('utf-8') \
or getattr(conf,'mail_login','')
self.port = port or cint(es.mail_port) \
or getattr(conf,'mail_port',None)
self.password = password or es.mail_password.encode('utf-8') \
or getattr(conf,'mail_password','')
self.use_ssl = use_ssl or cint(es.use_ssl) \
or cint(getattr(conf, 'use_ssl', ''))
self._sess = None
@property
def sess(self):
"""get session"""
if self._sess:
return self._sess
try:
sess.quit()
except:
pass
def smtp_connect(self):
"""
Gets a smtp connection and handles errors
"""
from webnotes.utils import cint
import smtplib
import _socket
@ -245,26 +241,26 @@ class EMail:
raise webnotes.OutgoingEmailError, err_msg
try:
sess = smtplib.SMTP(self.server, cint(self.port) or None)
self._sess = smtplib.SMTP(self.server, cint(self.port) or None)
if not sess:
if not self._sess:
err_msg = 'Could not connect to outgoing email server'
webnotes.msgprint(err_msg)
raise webnotes.OutgoingEmailError, err_msg
if self.use_ssl:
sess.ehlo()
sess.starttls()
sess.ehlo()
self._sess.ehlo()
self._sess.starttls()
self._sess.ehlo()
ret = sess.login(self.login, self.password)
ret = self._sess.login(self.login, self.password)
# check if logged correctly
if ret[0]!=235:
msgprint(ret[1])
raise webnotes.OutgoingEmailError, ret[1]
return sess
return self._sess
except _socket.error, e:
# Invalid mail server -- due to refusing connection
@ -276,4 +272,6 @@ class EMail:
except smtplib.SMTPException, e:
webnotes.msgprint('There is something wrong with your Outgoing Mail Settings. \
Please contact us at support@erpnext.com')
raise webnotes.OutgoingEmailError, e
raise webnotes.OutgoingEmailError, e