1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2009 Sharoon Thomas
6 # Copyright (C) 2004-2010 OpenERP SA (<http://www.openerp.com>)
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
18 # You should have received a copy of the GNU General Public License
19 # along with this program. If not, see <http://www.gnu.org/licenses/>
21 ##############################################################################
23 from osv import osv, fields
27 from email import Encoders
28 from email.mime.base import MIMEBase
29 from email.mime.multipart import MIMEMultipart
30 from email.mime.text import MIMEText
31 from email.header import decode_header, Header
32 from email.utils import formatdate
35 from tools.translate import _
39 EMAIL_PATTERN = re.compile(r'([^()\[\] ,<:\\>@";]+@[^()\[\] ,<:\\>@";]+)') # See RFC822
40 def extract_emails(emails_str):
42 Returns a list of email addresses recognized in a string, ignoring the rest of the string.
43 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']"
45 return EMAIL_PATTERN.findall(emails_str)
48 def extract_emails_from_dict(addresses={}):
50 Extracts email addresses from a dictionary with comma-separated address string values, handling
51 separately the To, CC, BCC and Reply-To addresses.
53 :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', ... }
54 :return: a dictionary with a list of separate addresses for each header (To, CC, BCC), with an additional key 'all-recipients'
55 containing all addresses for the 'To', 'CC', 'BCC' entries.
57 result = {'all-recipients':[]}
58 keys = ['To', 'CC', 'BCC', 'Reply-To']
60 emails = extract_emails(addresses.get(each, u''))
64 if each != 'Reply-To':
65 result['all-recipients'].extend(emails)
68 class email_template_account(osv.osv):
70 Object to store email account settings
72 _name = "email_template.account"
73 _known_content_types = ['multipart/mixed',
74 'multipart/alternative',
80 'name': fields.char('Description',
81 size=64, required=True,
82 readonly=True, select=True,
83 help="The description is used as the Sender name along with the provided From Email, \
84 unless it is already specified in the From Email, e.g: John Doe <john@doe.com>",
85 states={'draft':[('readonly', False)]}),
86 'auto_delete': fields.boolean('Auto Delete', size=64, readonly=True,
87 help="Permanently delete emails after sending",
88 states={'draft':[('readonly', False)]}),
89 'user':fields.many2one('res.users',
90 'Related User', required=True,
91 readonly=True, states={'draft':[('readonly', False)]}),
92 'email_id': fields.char('From Email',
93 size=120, required=True,
94 readonly=True, states={'draft':[('readonly', False)]} ,
95 help="eg: 'john@doe.com' or 'John Doe <john@doe.com>'"),
96 'smtpserver': fields.char('Server',
97 size=120, required=True,
98 readonly=True, states={'draft':[('readonly', False)]},
99 help="Enter name of outgoing server, eg: smtp.yourdomain.com"),
100 'smtpport': fields.integer('SMTP Port',
101 size=64, required=True,
102 readonly=True, states={'draft':[('readonly', False)]},
103 help="Enter port number, eg: 25 or 587"),
104 'smtpuname': fields.char('User Name',
105 size=120, required=False,
106 readonly=True, states={'draft':[('readonly', False)]},
107 help="Specify the username if your SMTP server requires authentication, "
108 "otherwise leave it empty."),
109 'smtppass': fields.char('Password',
110 size=120, invisible=True,
111 required=False, readonly=True,
112 states={'draft':[('readonly', False)]}),
113 'smtptls':fields.boolean('TLS',
114 states={'draft':[('readonly', False)]}, readonly=True),
116 'smtpssl':fields.boolean('SSL/TLS (only in python 2.6)',
117 states={'draft':[('readonly', False)]}, readonly=True),
118 'send_pref':fields.selection([
119 ('html', 'HTML, otherwise Text'),
120 ('text', 'Text, otherwise HTML'),
121 ('alternative', 'Both HTML & Text (Alternative)'),
122 ('mixed', 'Both HTML & Text (Mixed)')
123 ], 'Mail Format', required=True),
124 'company':fields.selection([
129 help="Select if this mail account does not belong " \
130 "to specific user but to the organization as a whole. " \
131 "eg: info@companydomain.com",
132 required=True, states={
133 'draft':[('readonly', False)]
136 'state':fields.selection([
137 ('draft', 'Initiated'),
138 ('suspended', 'Suspended'),
139 ('approved', 'Approved')
141 'State', required=True, readonly=True),
145 'name':lambda self, cursor, user, context:self.pool.get(
154 'state':lambda * a:'draft',
155 'smtpport':lambda *a:25,
156 'smtpserver':lambda *a:'localhost',
157 'company':lambda *a:'yes',
158 'user':lambda self, cursor, user, context:user,
159 'send_pref':lambda *a: 'html',
160 'smtptls':lambda *a:True,
167 'Another setting already exists with this email ID !')
170 def name_get(self, cr, uid, ids, context=None):
171 return [(a["id"], "%s (%s)" % (a['email_id'], a['name'])) for a in self.read(cr, uid, ids, ['name', 'email_id'], context=context)]
173 def _constraint_unique(self, cursor, user, ids, context=None):
175 This makes sure that you dont give personal
176 users two accounts with same ID (Validated in sql constaints)
177 However this constraint exempts company accounts.
178 Any no of co accounts for a user is allowed
180 if self.read(cursor, user, ids, ['company'])[0]['company'] == 'no':
181 accounts = self.search(cursor, user, [
183 ('company', '=', 'no')
185 if len(accounts) > 1 :
194 'Error: You are not allowed to have more than 1 account.',
198 def get_outgoing_server(self, cursor, user, ids, context=None):
200 Returns the Out Going Connection (SMTP) object
202 @attention: DO NOT USE except_osv IN THIS METHOD
203 @param cursor: Database Cursor
204 @param user: ID of current user
205 @param ids: ID/list of ids of current object for
206 which connection is required
207 First ID will be chosen from lists
208 @param context: Context
210 @return: SMTP server object or Exception
214 #Type cast ids to integer
215 if type(ids) == list:
217 this_object = self.browse(cursor, user, ids, context=context)
219 if this_object.smtpserver and this_object.smtpport:
221 if this_object.smtpssl:
222 serv = smtplib.SMTP_SSL(this_object.smtpserver, this_object.smtpport)
224 serv = smtplib.SMTP(this_object.smtpserver, this_object.smtpport)
225 if this_object.smtptls:
229 except Exception, error:
232 if serv.has_extn('AUTH') or this_object.smtpuname or this_object.smtppass:
233 serv.login(str(this_object.smtpuname), str(this_object.smtppass))
234 except Exception, error:
237 raise Exception(_("SMTP SERVER or PORT not specified"))
238 raise Exception(_("Core connection for the given ID does not exist"))
240 def check_outgoing_connection(self, cursor, user, ids, context=None):
242 checks SMTP credentials and confirms if outgoing connection works
244 @param cursor: Database Cursor
245 @param user: ID of current user
246 @param ids: list of ids of current object for
247 which connection is required
248 @param context: Context
251 self.get_outgoing_server(cursor, user, ids, context)
252 raise osv.except_osv(_("SMTP Test Connection Was Successful"), '')
253 except osv.except_osv, success_message:
254 raise success_message
255 except Exception, error:
256 raise osv.except_osv(
257 _("Out going connection test failed"),
258 _("Reason: %s") % error
261 def do_approval(self, cr, uid, ids, context=None):
262 #TODO: Check if user has rights
263 self.write(cr, uid, ids, {'state':'approved'}, context=context)
264 # wf_service = netsvc.LocalService("workflow")
266 def smtp_connection(self, cursor, user, id, context=None):
268 This method should now wrap smtp_connection
270 #This function returns a SMTP server object
273 logger = netsvc.Logger()
274 core_obj = self.browse(cursor, user, id, context=context)
275 if core_obj.smtpserver and core_obj.smtpport and core_obj.state == 'approved':
277 serv = self.get_outgoing_server(cursor, user, id, context)
278 except Exception, error:
279 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))
281 #Everything is complete, now return the connection
284 logger.notifyChannel(_("Email Template"), netsvc.LOG_ERROR, _("Mail from Account %s failed. Probable Reason:Account not approved") % id)
287 #**************************** MAIL SENDING FEATURES ***********************#
292 def send_mail(self, cr, uid, ids, addresses, subject='', body=None, payload=None, message_id=None, context=None):
293 #TODO: Replace all this with a single email object
300 logger = netsvc.Logger()
302 core_obj = self.browse(cr, uid, id, context)
303 serv = self.smtp_connection(cr, uid, id)
306 # Prepare multipart containers depending on data
307 text_subtype = (core_obj.send_pref == 'alternative') and 'alternative' or 'mixed'
308 # Need a multipart/mixed wrapper for attachments if content is alternative
309 if payload and text_subtype == 'alternative':
310 payload_part = MIMEMultipart(_subtype='mixed')
311 text_part = MIMEMultipart(_subtype=text_subtype)
312 payload_part.attach(text_part)
314 # otherwise a single multipart/mixed will do the whole job
315 payload_part = text_part = MIMEMultipart(_subtype=text_subtype)
318 payload_part['Subject'] = subject
319 from_email = core_obj.email_id
320 if '<' in from_email:
321 # We have a structured email address, keep it untouched
322 payload_part['From'] = Header(core_obj.email_id, 'utf-8').encode()
324 # Plain email address, construct a structured one based on the name:
325 sender_name = Header(core_obj.name, 'utf-8').encode()
326 payload_part['From'] = sender_name + " <" + core_obj.email_id + ">"
327 payload_part['Organization'] = tools.ustr(core_obj.user.company_id.name)
328 payload_part['Date'] = formatdate()
329 addresses_l = extract_emails_from_dict(addresses)
330 if addresses_l['To']:
331 payload_part['To'] = u','.join(addresses_l['To'])
332 if addresses_l['CC']:
333 payload_part['CC'] = u','.join(addresses_l['CC'])
334 if addresses_l['Reply-To']:
335 payload_part['Reply-To'] = addresses_l['Reply-To'][0]
337 payload_part['Message-ID'] = message_id
338 if body.get('text', False):
339 temp_body_text = body.get('text', '')
340 l = len(temp_body_text.replace(' ', '').replace('\r', '').replace('\n', ''))
342 body['text'] = u'No Mail Message'
343 # Attach parts into message container.
344 # According to RFC 2046, the last part of a multipart message, in this case
345 # the HTML message, is best and preferred.
346 if core_obj.send_pref in ('text', 'mixed', 'alternative'):
347 body_text = body.get('text', u'<Empty Message>')
348 body_text = tools.ustr(body_text)
349 text_part.attach(MIMEText(body_text.encode("utf-8"), _charset='UTF-8'))
350 if core_obj.send_pref in ('html', 'mixed', 'alternative'):
351 html_body = body.get('html', u'')
352 if len(html_body) == 0 or html_body == u'':
353 html_body = body.get('text', u'<p><Empty Message></p>').replace('\n', '<br/>').replace('\r', '<br/>')
354 html_body = tools.ustr(html_body)
355 text_part.attach(MIMEText(html_body.encode("utf-8"), _subtype='html', _charset='UTF-8'))
357 #Now add attachments if any, wrapping into a container multipart/mixed if needed
360 part = MIMEBase('application', "octet-stream")
361 part.set_payload(base64.decodestring(payload[file]))
362 part.add_header('Content-Disposition', 'attachment; filename="%s"' % file)
363 Encoders.encode_base64(part)
364 payload_part.attach(part)
365 except Exception, error:
366 logger.notifyChannel(_("Email Template"), netsvc.LOG_ERROR, _("Mail from Account %s failed. Probable Reason:MIME Error\nDescription: %s") % (id, error))
367 return {'error_msg': "Server Send Error\nDescription: %s"%error}
369 serv.sendmail(payload_part['From'], addresses_l['all-recipients'], payload_part.as_string())
370 except Exception, error:
371 logging.getLogger('email_template').error("Mail from Account %s failed. Probable Reason: Server Send Error\n Description: %s", id, error, exc_info=True)
372 return {'error_msg': "Server Send Error\nDescription: %s"%error}
373 #The mail sending is complete
375 logger.notifyChannel(_("Email Template"), netsvc.LOG_INFO, _("Mail from Account %s successfully Sent.") % (id))
378 logger.notifyChannel(_("Email Template"), netsvc.LOG_ERROR, _("Mail from Account %s failed. Probable Reason:Account not approved") % id)
379 return {'error_msg':"Mail from Account %s failed. Probable Reason:Account not approved"% id}
381 def extracttime(self, time_as_string):
385 logger = netsvc.Logger()
386 #The standard email dates are of format similar to:
387 #Thu, 8 Oct 2009 09:35:42 +0200
389 convertor = {'+':1, '-':-1}
391 time_as_string = time_as_string.replace(',', '')
392 date_list = time_as_string.split(' ')
393 date_temp_str = ' '.join(date_list[1:5])
394 if len(date_list) >= 6:
395 sign = convertor.get(date_list[5][0], False)
399 dt = datetime.datetime.strptime(
404 dt = datetime.datetime.strptime(
411 offset = datetime.timedelta(
419 except Exception, e2:
420 """Looks like UT or GMT, just forget decoding"""
423 offset = datetime.timedelta(hours=0)
425 date_as_date = dt.strftime('%Y-%m-%d %H:%M:%S')
427 logger.notifyChannel(
431 "Datetime Extraction failed.Date:%s \
438 def send_receive(self, cr, uid, ids, context=None):
441 ctx['filters'] = [('account_id', '=', id)]
442 self.pool.get('email_template.mailbox').send_all_mail(cr, uid, [], context=ctx)
445 def decode_header_text(self, text):
446 """ Decode internationalized headers RFC2822.
447 To, CC, BCC, Subject fields can contain
448 text slices with different encodes, like:
449 =?iso-8859-1?Q?Enric_Mart=ED?= <enricmarti@company.com>,
450 =?Windows-1252?Q?David_G=F3mez?= <david@company.com>
451 Sometimes they include extra " character at the beginning/
452 end of the contact name, like:
453 "=?iso-8859-1?Q?Enric_Mart=ED?=" <enricmarti@company.com>
454 and decode_header() does not work well, so we use regular
455 expressions (?= ? ? ?=) to split the text slices
459 p = re.compile("(=\?.*?\?.\?.*?\?=)")
462 for t2 in p.split(text):
466 ) for (s, t) in decode_header(t2)]
472 email_template_account()
474 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: