+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2009 Sharoon Thomas
+# Copyright (C) 2004-2010 OpenERP SA (<http://www.openerp.com>)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>
+#
+##############################################################################
+
from osv import osv, fields
-from html2text import html2text
import re
import smtplib
import base64
from email.mime.text import MIMEText
from email.header import decode_header, Header
from email.utils import formatdate
-import re
import netsvc
-import string
-import email
-import time, datetime
-import email_template_engines
+import datetime
from tools.translate import _
import tools
+import logging
+
+EMAIL_PATTERN = re.compile(r'([^()\[\] ,<:\\>@";]+@[^()\[\] ,<:\\>@";]+)') # See RFC822
+def extract_emails(emails_str):
+ """
+ Returns a list of email addresses recognized in a string, ignoring the rest of the string.
+ extract_emails('a@b.com,c@bcom, "John Doe" <d@b.com> , e@b.com') -> ['a@b.com','c@bcom', 'd@b.com', 'e@b.com']"
+ """
+ return EMAIL_PATTERN.findall(emails_str)
+
+
+def extract_emails_from_dict(addresses={}):
+ """
+ Extracts email addresses from a dictionary with comma-separated address string values, handling
+ separately the To, CC, BCC and Reply-To addresses.
+
+ :param addresses: a dictionary of addresses in the form {'To': 'a@b.com,c@bcom; d@b.com;e@b.com' , 'CC': 'e@b.com;f@b.com', ... }
+ :return: a dictionary with a list of separate addresses for each header (To, CC, BCC), with an additional key 'all-recipients'
+ containing all addresses for the 'To', 'CC', 'BCC' entries.
+ """
+ result = {'all-recipients':[]}
+ keys = ['To', 'CC', 'BCC', 'Reply-To']
+ for each in keys:
+ emails = extract_emails(addresses.get(each, u''))
+ while u'' in emails:
+ emails.remove(u'')
+ result[each] = emails
+ if each != 'Reply-To':
+ result['all-recipients'].extend(emails)
+ return result
class email_template_account(osv.osv):
"""
'name': fields.char('Description',
size=64, required=True,
readonly=True, select=True,
+ help="The description is used as the Sender name along with the provided From Email, \
+unless it is already specified in the From Email, e.g: John Doe <john@doe.com>",
states={'draft':[('readonly', False)]}),
+ 'auto_delete': fields.boolean('Auto Delete', size=64, readonly=True,
+ help="Permanently delete emails after sending",
+ states={'draft':[('readonly', False)]}),
'user':fields.many2one('res.users',
'Related User', required=True,
readonly=True, states={'draft':[('readonly', False)]}),
- 'email_id': fields.char('Email ID',
+ 'email_id': fields.char('From Email',
size=120, required=True,
readonly=True, states={'draft':[('readonly', False)]} ,
- help=" eg:yourname@yourdomain.com "),
+ help="eg: 'john@doe.com' or 'John Doe <john@doe.com>'"),
'smtpserver': fields.char('Server',
size=120, required=True,
readonly=True, states={'draft':[('readonly', False)]},
- help="Enter name of outgoing server,eg:smtp.gmail.com "),
- 'smtpport': fields.integer('SMTP Port ',
+ help="Enter name of outgoing server, eg: smtp.yourdomain.com"),
+ 'smtpport': fields.integer('SMTP Port',
size=64, required=True,
readonly=True, states={'draft':[('readonly', False)]},
- help="Enter port number,eg:SMTP-587 "),
+ help="Enter port number, eg: 25 or 587"),
'smtpuname': fields.char('User Name',
size=120, required=False,
- readonly=True, states={'draft':[('readonly', False)]}),
+ readonly=True, states={'draft':[('readonly', False)]},
+ help="Specify the username if your SMTP server requires authentication, "
+ "otherwise leave it empty."),
'smtppass': fields.char('Password',
size=120, invisible=True,
required=False, readonly=True,
'smtpssl':fields.boolean('SSL/TLS (only in python 2.6)',
states={'draft':[('readonly', False)]}, readonly=True),
'send_pref':fields.selection([
- ('html', 'HTML otherwise Text'),
- ('text', 'Text otherwise HTML'),
- ('both', 'Both HTML & Text')
+ ('html', 'HTML, otherwise Text'),
+ ('text', 'Text, otherwise HTML'),
+ ('alternative', 'Both HTML & Text (Alternative)'),
+ ('mixed', 'Both HTML & Text (Mixed)')
], 'Mail Format', required=True),
'company':fields.selection([
('yes', 'Yes'),
('no', 'No')
- ], 'Company Mail A/c',
+ ], 'Corporate',
readonly=True,
- help="Select if this mail account does not belong" \
- "to specific user but the organisation as a whole." \
- "eg:info@somedomain.com",
+ help="Select if this mail account does not belong " \
+ "to specific user but to the organization as a whole. " \
+ "eg: info@companydomain.com",
required=True, states={
'draft':[('readonly', False)]
}),
('suspended', 'Suspended'),
('approved', 'Approved')
],
- 'Status', required=True, readonly=True),
+ 'State', required=True, readonly=True),
}
_defaults = {
context
)['name'],
'state':lambda * a:'draft',
+ 'smtpport':lambda *a:25,
+ 'smtpserver':lambda *a:'localhost',
+ 'company':lambda *a:'yes',
'user':lambda self, cursor, user, context:user,
- 'send_pref':lambda * a: 'html',
- 'smtptls':lambda * a:True,
+ 'send_pref':lambda *a: 'html',
+ 'smtptls':lambda *a:True,
}
_sql_constraints = [
'unique (email_id)',
'Another setting already exists with this email ID !')
]
-
- def _constraint_unique(self, cursor, user, ids):
+
+ def name_get(self, cr, uid, ids, context=None):
+ return [(a["id"], "%s (%s)" % (a['email_id'], a['name'])) for a in self.read(cr, uid, ids, ['name', 'email_id'], context=context)]
+
+ def _constraint_unique(self, cursor, user, ids, context=None):
"""
This makes sure that you dont give personal
users two accounts with same ID (Validated in sql constaints)
'Error: You are not allowed to have more than 1 account.',
[])
]
-
- def on_change_emailid(self, cursor, user, ids, name=None, email_id=None, context=None):
- """
- Called when the email ID field changes.
-
- UI enhancement
- Writes the same email value to the smtpusername
- and incoming username
- """
- #TODO: Check and remove the write. Is it needed?
- self.write(cursor, user, ids, {'state':'draft'}, context=context)
- return {
- 'value': {
- 'state': 'draft',
- 'smtpuname':email_id,
- 'isuser':email_id
- }
- }
-
+
def get_outgoing_server(self, cursor, user, ids, context=None):
"""
Returns the Out Going Connection (SMTP) object
#Type cast ids to integer
if type(ids) == list:
ids = ids[0]
- this_object = self.browse(cursor, user, ids, context)
+ this_object = self.browse(cursor, user, ids, context=context)
if this_object:
if this_object.smtpserver and this_object.smtpport:
try:
raise error
try:
if serv.has_extn('AUTH') or this_object.smtpuname or this_object.smtppass:
- serv.login(this_object.smtpuname, this_object.smtppass)
+ serv.login(str(this_object.smtpuname), str(this_object.smtppass))
except Exception, error:
raise error
return serv
except Exception, error:
raise osv.except_osv(
_("Out going connection test failed"),
- _("Reason: %s") % error
+ _("Reason: %s") % tools.ustr(error)
)
- def do_approval(self, cr, uid, ids, context={}):
+ def do_approval(self, cr, uid, ids, context=None):
#TODO: Check if user has rights
self.write(cr, uid, ids, {'state':'approved'}, context=context)
# wf_service = netsvc.LocalService("workflow")
"""
#This function returns a SMTP server object
logger = netsvc.Logger()
- core_obj = self.browse(cursor, user, id, context)
+ core_obj = self.browse(cursor, user, id, context=context)
if core_obj.smtpserver and core_obj.smtpport and core_obj.state == 'approved':
try:
serv = self.get_outgoing_server(cursor, user, id, context)
except Exception, error:
- logger.notifyChannel(_("Email Template"), netsvc.LOG_ERROR, _("Mail from Account %s failed on login. Probable Reason:Could not login to server\nError: %s") % (id, error))
+ logger.notifyChannel(_("Email Template"), netsvc.LOG_ERROR, _("Mail from Account %s failed on login. Probable Reason:Could not login to server\nError: %s") % (id, tools.ustr(error)))
return False
#Everything is complete, now return the connection
return serv
return False
#**************************** MAIL SENDING FEATURES ***********************#
- def split_to_ids(self, ids_as_str):
- """
- Identifies email IDs separated by separators
- and returns a list
- TODO: Doc this
- "a@b.com,c@bcom; d@b.com;e@b.com->['a@b.com',...]"
- """
- email_sep_by_commas = ids_as_str \
- .replace('; ', ',') \
- .replace(';', ',') \
- .replace(', ', ',')
- return email_sep_by_commas.split(',')
-
- def get_ids_from_dict(self, addresses={}):
- """
- TODO: Doc this
- """
- result = {'all':[]}
- keys = ['To', 'CC', 'BCC']
- for each in keys:
- ids_as_list = self.split_to_ids(addresses.get(each, u''))
- while u'' in ids_as_list:
- ids_as_list.remove(u'')
- result[each] = ids_as_list
- result['all'].extend(ids_as_list)
- return result
+
+
+
- def send_mail(self, cr, uid, ids, addresses, subject='', body=None, payload=None, context=None):
- #TODO: Replace all this crap with a single email object
+ def send_mail(self, cr, uid, ids, addresses, subject='', body=None, payload=None, message_id=None, context=None):
+ #TODO: Replace all this with a single email object
if body is None:
body = {}
if payload is None:
serv = self.smtp_connection(cr, uid, id)
if serv:
try:
- msg = MIMEMultipart()
+ # Prepare multipart containers depending on data
+ text_subtype = (core_obj.send_pref == 'alternative') and 'alternative' or 'mixed'
+ # Need a multipart/mixed wrapper for attachments if content is alternative
+ if payload and text_subtype == 'alternative':
+ payload_part = MIMEMultipart(_subtype='mixed')
+ text_part = MIMEMultipart(_subtype=text_subtype)
+ payload_part.attach(text_part)
+ else:
+ # otherwise a single multipart/mixed will do the whole job
+ payload_part = text_part = MIMEMultipart(_subtype=text_subtype)
+
if subject:
- msg['Subject'] = subject
- sender_name = Header(core_obj.name, 'utf-8').encode()
- msg['From'] = sender_name + " <" + core_obj.email_id + ">"
- msg['Organization'] = tools.ustr(core_obj.user.company_id.name)
- msg['Date'] = formatdate()
- addresses_l = self.get_ids_from_dict(addresses)
+ payload_part['Subject'] = subject
+ from_email = core_obj.email_id
+ if '<' in from_email:
+ # We have a structured email address, keep it untouched
+ payload_part['From'] = Header(core_obj.email_id, 'utf-8').encode()
+ else:
+ # Plain email address, construct a structured one based on the name:
+ sender_name = Header(core_obj.name, 'utf-8').encode()
+ payload_part['From'] = sender_name + " <" + core_obj.email_id + ">"
+ payload_part['Organization'] = tools.ustr(core_obj.user.company_id.name)
+ payload_part['Date'] = formatdate()
+ addresses_l = extract_emails_from_dict(addresses)
if addresses_l['To']:
- msg['To'] = u','.join(addresses_l['To'])
+ payload_part['To'] = u','.join(addresses_l['To'])
if addresses_l['CC']:
- msg['CC'] = u','.join(addresses_l['CC'])
-# if addresses_l['BCC']:
-# msg['BCC'] = u','.join(addresses_l['BCC'])
+ payload_part['CC'] = u','.join(addresses_l['CC'])
+ if addresses_l['Reply-To']:
+ payload_part['Reply-To'] = addresses_l['Reply-To'][0]
+ if message_id:
+ payload_part['Message-ID'] = message_id
if body.get('text', False):
temp_body_text = body.get('text', '')
l = len(temp_body_text.replace(' ', '').replace('\r', '').replace('\n', ''))
# Attach parts into message container.
# According to RFC 2046, the last part of a multipart message, in this case
# the HTML message, is best and preferred.
- if core_obj.send_pref == 'text' or core_obj.send_pref == 'both':
- body_text = body.get('text', u'No Mail Message')
+ if core_obj.send_pref in ('text', 'mixed', 'alternative'):
+ body_text = body.get('text', u'<Empty Message>')
body_text = tools.ustr(body_text)
- msg.attach(MIMEText(body_text.encode("utf-8"), _charset='UTF-8'))
- if core_obj.send_pref == 'html' or core_obj.send_pref == 'both':
+ text_part.attach(MIMEText(body_text.encode("utf-8"), _charset='UTF-8'))
+ if core_obj.send_pref in ('html', 'mixed', 'alternative'):
html_body = body.get('html', u'')
if len(html_body) == 0 or html_body == u'':
- html_body = body.get('text', u'<p>No Mail Message</p>').replace('\n', '<br/>').replace('\r', '<br/>')
+ html_body = body.get('text', u'<p><Empty Message></p>').replace('\n', '<br/>').replace('\r', '<br/>')
html_body = tools.ustr(html_body)
- msg.attach(MIMEText(html_body.encode("utf-8"), _subtype='html', _charset='UTF-8'))
- #Now add attachments if any
- for file in payload.keys():
- part = MIMEBase('application', "octet-stream")
- part.set_payload(base64.decodestring(payload[file]))
- part.add_header('Content-Disposition', 'attachment; filename="%s"' % file)
- Encoders.encode_base64(part)
- msg.attach(part)
+ text_part.attach(MIMEText(html_body.encode("utf-8"), _subtype='html', _charset='UTF-8'))
+
+ #Now add attachments if any, wrapping into a container multipart/mixed if needed
+ if payload:
+ for file in payload:
+ part = MIMEBase('application', "octet-stream")
+ part.set_payload(base64.decodestring(payload[file]))
+ part.add_header('Content-Disposition', 'attachment; filename="%s"' % file)
+ Encoders.encode_base64(part)
+ payload_part.attach(part)
except Exception, error:
- logger.notifyChannel(_("Email Template"), netsvc.LOG_ERROR, _("Mail from Account %s failed. Probable Reason:MIME Error\nDescription: %s") % (id, error))
- return error
+ logger.notifyChannel(_("Email Template"), netsvc.LOG_ERROR, _("Mail from Account %s failed. Probable Reason:MIME Error\nDescription: %s") % (id, tools.ustr(error)))
+ return {'error_msg': _("Server Send Error\nDescription: %s")%error}
try:
- #print msg['From'],toadds
- serv.sendmail(msg['From'], addresses_l['all'], msg.as_string())
+ serv.sendmail(payload_part['From'], addresses_l['all-recipients'], payload_part.as_string())
except Exception, error:
- logger.notifyChannel(_("Email Template"), netsvc.LOG_ERROR, _("Mail from Account %s failed. Probable Reason:Server Send Error\nDescription: %s") % (id, error))
- return error
+ logging.getLogger('email_template').error(_("Mail from Account %s failed. Probable Reason: Server Send Error\n Description: %s"), id, tools.ustr(error), exc_info=True)
+ return {'error_msg': _("Server Send Error\nDescription: %s") % tools.ustr(error)}
#The mail sending is complete
serv.close()
logger.notifyChannel(_("Email Template"), netsvc.LOG_INFO, _("Mail from Account %s successfully Sent.") % (id))
return True
else:
logger.notifyChannel(_("Email Template"), netsvc.LOG_ERROR, _("Mail from Account %s failed. Probable Reason:Account not approved") % id)
-
+ return {'nodestroy':True,'error_msg': _("Mail from Account %s failed. Probable Reason:Account not approved")% id}
+
def extracttime(self, time_as_string):
"""
TODO: DOC THis
logger = netsvc.Logger()
#The standard email dates are of format similar to:
#Thu, 8 Oct 2009 09:35:42 +0200
- #print time_as_string
date_as_date = False
convertor = {'+':1, '-':-1}
try:
offset = datetime.timedelta(hours=0)
dt = dt + offset
date_as_date = dt.strftime('%Y-%m-%d %H:%M:%S')
- #print date_as_date
except Exception, e:
logger.notifyChannel(
_("Email Template"),
"Datetime Extraction failed.Date:%s \
\tError:%s") % (
time_as_string,
- e)
+ tools.ustr(e))
)
return date_as_date
def send_receive(self, cr, uid, ids, context=None):
- self.get_mails(cr, uid, ids, context)
for id in ids:
ctx = context.copy()
ctx['filters'] = [('account_id', '=', id)]