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
212 #Type cast ids to integer
213 if type(ids) == list:
215 this_object = self.browse(cursor, user, ids, context=context)
217 if this_object.smtpserver and this_object.smtpport:
219 if this_object.smtpssl:
220 serv = smtplib.SMTP_SSL(this_object.smtpserver, this_object.smtpport)
222 serv = smtplib.SMTP(this_object.smtpserver, this_object.smtpport)
223 if this_object.smtptls:
227 except Exception, error:
230 if serv.has_extn('AUTH') or this_object.smtpuname or this_object.smtppass:
231 serv.login(str(this_object.smtpuname), str(this_object.smtppass))
232 except Exception, error:
235 raise Exception(_("SMTP SERVER or PORT not specified"))
236 raise Exception(_("Core connection for the given ID does not exist"))
238 def check_outgoing_connection(self, cursor, user, ids, context=None):
240 checks SMTP credentials and confirms if outgoing connection works
242 @param cursor: Database Cursor
243 @param user: ID of current user
244 @param ids: list of ids of current object for
245 which connection is required
246 @param context: Context
249 self.get_outgoing_server(cursor, user, ids, context)
250 raise osv.except_osv(_("SMTP Test Connection Was Successful"), '')
251 except osv.except_osv, success_message:
252 raise success_message
253 except Exception, error:
254 raise osv.except_osv(
255 _("Out going connection test failed"),
256 _("Reason: %s") % tools.ustr(error)
259 def do_approval(self, cr, uid, ids, context=None):
260 #TODO: Check if user has rights
261 self.write(cr, uid, ids, {'state':'approved'}, context=context)
262 # wf_service = netsvc.LocalService("workflow")
264 def smtp_connection(self, cursor, user, id, context=None):
266 This method should now wrap smtp_connection
268 #This function returns a SMTP server object
269 logger = netsvc.Logger()
270 core_obj = self.browse(cursor, user, id, context=context)
271 if core_obj.smtpserver and core_obj.smtpport and core_obj.state == 'approved':
273 serv = self.get_outgoing_server(cursor, user, id, context)
274 except Exception, error:
275 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)))
277 #Everything is complete, now return the connection
280 logger.notifyChannel(_("Email Template"), netsvc.LOG_ERROR, _("Mail from Account %s failed. Probable Reason:Account not approved") % id)
283 #**************************** MAIL SENDING FEATURES ***********************#
288 def send_mail(self, cr, uid, ids, addresses, subject='', body=None, payload=None, message_id=None, context=None):
289 #TODO: Replace all this with a single email object
296 logger = netsvc.Logger()
298 core_obj = self.browse(cr, uid, id, context)
299 serv = self.smtp_connection(cr, uid, id)
302 # Prepare multipart containers depending on data
303 text_subtype = (core_obj.send_pref == 'alternative') and 'alternative' or 'mixed'
304 # Need a multipart/mixed wrapper for attachments if content is alternative
305 if payload and text_subtype == 'alternative':
306 payload_part = MIMEMultipart(_subtype='mixed')
307 text_part = MIMEMultipart(_subtype=text_subtype)
308 payload_part.attach(text_part)
310 # otherwise a single multipart/mixed will do the whole job
311 payload_part = text_part = MIMEMultipart(_subtype=text_subtype)
314 payload_part['Subject'] = subject
315 from_email = core_obj.email_id
316 if '<' in from_email:
317 # We have a structured email address, keep it untouched
318 payload_part['From'] = Header(core_obj.email_id, 'utf-8').encode()
320 # Plain email address, construct a structured one based on the name:
321 sender_name = Header(core_obj.name, 'utf-8').encode()
322 payload_part['From'] = sender_name + " <" + core_obj.email_id + ">"
323 payload_part['Organization'] = tools.ustr(core_obj.user.company_id.name)
324 payload_part['Date'] = formatdate()
325 addresses_l = extract_emails_from_dict(addresses)
326 if addresses_l['To']:
327 payload_part['To'] = u','.join(addresses_l['To'])
328 if addresses_l['CC']:
329 payload_part['CC'] = u','.join(addresses_l['CC'])
330 if addresses_l['Reply-To']:
331 payload_part['Reply-To'] = addresses_l['Reply-To'][0]
333 payload_part['Message-ID'] = message_id
334 if body.get('text', False):
335 temp_body_text = body.get('text', '')
336 l = len(temp_body_text.replace(' ', '').replace('\r', '').replace('\n', ''))
338 body['text'] = u'No Mail Message'
339 # Attach parts into message container.
340 # According to RFC 2046, the last part of a multipart message, in this case
341 # the HTML message, is best and preferred.
342 if core_obj.send_pref in ('text', 'mixed', 'alternative'):
343 body_text = body.get('text', u'<Empty Message>')
344 body_text = tools.ustr(body_text)
345 text_part.attach(MIMEText(body_text.encode("utf-8"), _charset='UTF-8'))
346 if core_obj.send_pref in ('html', 'mixed', 'alternative'):
347 html_body = body.get('html', u'')
348 if len(html_body) == 0 or html_body == u'':
349 html_body = body.get('text', u'<p><Empty Message></p>').replace('\n', '<br/>').replace('\r', '<br/>')
350 html_body = tools.ustr(html_body)
351 text_part.attach(MIMEText(html_body.encode("utf-8"), _subtype='html', _charset='UTF-8'))
353 #Now add attachments if any, wrapping into a container multipart/mixed if needed
356 part = MIMEBase('application', "octet-stream")
357 part.set_payload(base64.decodestring(payload[file]))
358 part.add_header('Content-Disposition', 'attachment; filename="%s"' % file)
359 Encoders.encode_base64(part)
360 payload_part.attach(part)
361 except Exception, error:
362 logger.notifyChannel(_("Email Template"), netsvc.LOG_ERROR, _("Mail from Account %s failed. Probable Reason:MIME Error\nDescription: %s") % (id, tools.ustr(error)))
363 return {'error_msg': _("Server Send Error\nDescription: %s")%error}
365 serv.sendmail(payload_part['From'], addresses_l['all-recipients'], payload_part.as_string())
366 except Exception, error:
367 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)
368 return {'error_msg': _("Server Send Error\nDescription: %s") % tools.ustr(error)}
369 #The mail sending is complete
371 logger.notifyChannel(_("Email Template"), netsvc.LOG_INFO, _("Mail from Account %s successfully Sent.") % (id))
374 logger.notifyChannel(_("Email Template"), netsvc.LOG_ERROR, _("Mail from Account %s failed. Probable Reason:Account not approved") % id)
375 return {'nodestroy':True,'error_msg': _("Mail from Account %s failed. Probable Reason:Account not approved")% id}
377 def extracttime(self, time_as_string):
381 logger = netsvc.Logger()
382 #The standard email dates are of format similar to:
383 #Thu, 8 Oct 2009 09:35:42 +0200
385 convertor = {'+':1, '-':-1}
387 time_as_string = time_as_string.replace(',', '')
388 date_list = time_as_string.split(' ')
389 date_temp_str = ' '.join(date_list[1:5])
390 if len(date_list) >= 6:
391 sign = convertor.get(date_list[5][0], False)
395 dt = datetime.datetime.strptime(
400 dt = datetime.datetime.strptime(
407 offset = datetime.timedelta(
415 except Exception, e2:
416 """Looks like UT or GMT, just forget decoding"""
419 offset = datetime.timedelta(hours=0)
421 date_as_date = dt.strftime('%Y-%m-%d %H:%M:%S')
423 logger.notifyChannel(
427 "Datetime Extraction failed.Date:%s \
434 def send_receive(self, cr, uid, ids, context=None):
437 ctx['filters'] = [('account_id', '=', id)]
438 self.pool.get('email_template.mailbox').send_all_mail(cr, uid, [], context=ctx)
441 def decode_header_text(self, text):
442 """ Decode internationalized headers RFC2822.
443 To, CC, BCC, Subject fields can contain
444 text slices with different encodes, like:
445 =?iso-8859-1?Q?Enric_Mart=ED?= <enricmarti@company.com>,
446 =?Windows-1252?Q?David_G=F3mez?= <david@company.com>
447 Sometimes they include extra " character at the beginning/
448 end of the contact name, like:
449 "=?iso-8859-1?Q?Enric_Mart=ED?=" <enricmarti@company.com>
450 and decode_header() does not work well, so we use regular
451 expressions (?= ? ? ?=) to split the text slices
455 p = re.compile("(=\?.*?\?.\?.*?\?=)")
458 for t2 in p.split(text):
462 ) for (s, t) in decode_header(t2)]
468 email_template_account()
470 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: