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 states={'draft':[('readonly', False)]}),
84 'auto_delete': fields.boolean('Auto Delete', size=64, readonly=True,
85 help="Permanently delete emails after sending",
86 states={'draft':[('readonly', False)]}),
87 'user':fields.many2one('res.users',
88 'Related User', required=True,
89 readonly=True, states={'draft':[('readonly', False)]}),
90 'email_id': fields.char('From Email',
91 size=120, required=True,
92 readonly=True, states={'draft':[('readonly', False)]} ,
93 help="eg: yourname@yourdomain.com "),
94 'smtpserver': fields.char('Server',
95 size=120, required=True,
96 readonly=True, states={'draft':[('readonly', False)]},
97 help="Enter name of outgoing server, eg:smtp.gmail.com "),
98 'smtpport': fields.integer('SMTP Port ',
99 size=64, required=True,
100 readonly=True, states={'draft':[('readonly', False)]},
101 help="Enter port number,eg:SMTP-587 "),
102 'smtpuname': fields.char('User Name',
103 size=120, required=False,
104 readonly=True, states={'draft':[('readonly', False)]},
105 help="Specify the username if your SMTP server requires authentication, "
106 "otherwise leave it empty."),
107 'smtppass': fields.char('Password',
108 size=120, invisible=True,
109 required=False, readonly=True,
110 states={'draft':[('readonly', False)]}),
111 'smtptls':fields.boolean('TLS',
112 states={'draft':[('readonly', False)]}, readonly=True),
114 'smtpssl':fields.boolean('SSL/TLS (only in python 2.6)',
115 states={'draft':[('readonly', False)]}, readonly=True),
116 'send_pref':fields.selection([
117 ('html', 'HTML, otherwise Text'),
118 ('text', 'Text, otherwise HTML'),
119 ('alternative', 'Both HTML & Text (Alternative)'),
120 ('mixed', 'Both HTML & Text (Mixed)')
121 ], 'Mail Format', required=True),
122 'company':fields.selection([
127 help="Select if this mail account does not belong " \
128 "to specific user but to the organization as a whole. " \
129 "eg: info@companydomain.com",
130 required=True, states={
131 'draft':[('readonly', False)]
134 'state':fields.selection([
135 ('draft', 'Initiated'),
136 ('suspended', 'Suspended'),
137 ('approved', 'Approved')
139 'Status', required=True, readonly=True),
143 'name':lambda self, cursor, user, context:self.pool.get(
152 'state':lambda * a:'draft',
153 'smtpport':lambda *a:25,
154 'smtpserver':lambda *a:'localhost',
155 'company':lambda *a:'yes',
156 'user':lambda self, cursor, user, context:user,
157 'send_pref':lambda *a: 'html',
158 'smtptls':lambda *a:True,
165 'Another setting already exists with this email ID !')
168 def name_get(self, cr, uid, ids, context=None):
169 return [(a["id"], "%s (%s)" % (a['email_id'], a['name'])) for a in self.read(cr, uid, ids, ['name', 'email_id'], context=context)]
171 def _constraint_unique(self, cursor, user, ids):
173 This makes sure that you dont give personal
174 users two accounts with same ID (Validated in sql constaints)
175 However this constraint exempts company accounts.
176 Any no of co accounts for a user is allowed
178 if self.read(cursor, user, ids, ['company'])[0]['company'] == 'no':
179 accounts = self.search(cursor, user, [
181 ('company', '=', 'no')
183 if len(accounts) > 1 :
192 'Error: You are not allowed to have more than 1 account.',
196 def get_outgoing_server(self, cursor, user, ids, context=None):
198 Returns the Out Going Connection (SMTP) object
200 @attention: DO NOT USE except_osv IN THIS METHOD
201 @param cursor: Database Cursor
202 @param user: ID of current user
203 @param ids: ID/list of ids of current object for
204 which connection is required
205 First ID will be chosen from lists
206 @param context: Context
208 @return: SMTP server object or Exception
210 #Type cast ids to integer
211 if type(ids) == list:
213 this_object = self.browse(cursor, user, ids, context)
215 if this_object.smtpserver and this_object.smtpport:
217 if this_object.smtpssl:
218 serv = smtplib.SMTP_SSL(this_object.smtpserver, this_object.smtpport)
220 serv = smtplib.SMTP(this_object.smtpserver, this_object.smtpport)
221 if this_object.smtptls:
225 except Exception, error:
228 if serv.has_extn('AUTH') or this_object.smtpuname or this_object.smtppass:
229 serv.login(str(this_object.smtpuname), str(this_object.smtppass))
230 except Exception, error:
233 raise Exception(_("SMTP SERVER or PORT not specified"))
234 raise Exception(_("Core connection for the given ID does not exist"))
236 def check_outgoing_connection(self, cursor, user, ids, context=None):
238 checks SMTP credentials and confirms if outgoing connection works
240 @param cursor: Database Cursor
241 @param user: ID of current user
242 @param ids: list of ids of current object for
243 which connection is required
244 @param context: Context
247 self.get_outgoing_server(cursor, user, ids, context)
248 raise osv.except_osv(_("SMTP Test Connection Was Successful"), '')
249 except osv.except_osv, success_message:
250 raise success_message
251 except Exception, error:
252 raise osv.except_osv(
253 _("Out going connection test failed"),
254 _("Reason: %s") % error
257 def do_approval(self, cr, uid, ids, context={}):
258 #TODO: Check if user has rights
259 self.write(cr, uid, ids, {'state':'approved'}, context=context)
260 # wf_service = netsvc.LocalService("workflow")
262 def smtp_connection(self, cursor, user, id, context=None):
264 This method should now wrap smtp_connection
266 #This function returns a SMTP server object
267 logger = netsvc.Logger()
268 core_obj = self.browse(cursor, user, id, context)
269 if core_obj.smtpserver and core_obj.smtpport and core_obj.state == 'approved':
271 serv = self.get_outgoing_server(cursor, user, id, context)
272 except Exception, error:
273 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))
275 #Everything is complete, now return the connection
278 logger.notifyChannel(_("Email Template"), netsvc.LOG_ERROR, _("Mail from Account %s failed. Probable Reason:Account not approved") % id)
281 #**************************** MAIL SENDING FEATURES ***********************#
286 def send_mail(self, cr, uid, ids, addresses, subject='', body=None, payload=None, message_id=None, context=None):
287 #TODO: Replace all this with a single email object
294 logger = netsvc.Logger()
296 core_obj = self.browse(cr, uid, id, context)
297 serv = self.smtp_connection(cr, uid, id)
300 # Prepare multipart containers depending on data
301 text_subtype = (core_obj.send_pref == 'alternative') and 'alternative' or 'mixed'
302 # Need a multipart/mixed wrapper for attachments if content is alternative
303 if payload and text_subtype == 'alternative':
304 payload_part = MIMEMultipart(_subtype='mixed')
305 text_part = MIMEMultipart(_subtype=text_subtype)
306 payload_part.attach(text_part)
308 # otherwise a single multipart/mixed will do the whole job
309 payload_part = text_part = MIMEMultipart(_subtype=text_subtype)
312 payload_part['Subject'] = subject
313 from_email = core_obj.email_id
314 if '<' in from_email:
315 # We have a structured email address, keep it untouched
316 payload_part['From'] = Header(core_obj.email_id, 'utf-8').encode()
318 # Plain email address, construct a structured one based on the name:
319 sender_name = Header(core_obj.name, 'utf-8').encode()
320 payload_part['From'] = sender_name + " <" + core_obj.email_id + ">"
321 payload_part['Organization'] = tools.ustr(core_obj.user.company_id.name)
322 payload_part['Date'] = formatdate()
323 addresses_l = extract_emails_from_dict(addresses)
324 if addresses_l['To']:
325 payload_part['To'] = u','.join(addresses_l['To'])
326 if addresses_l['CC']:
327 payload_part['CC'] = u','.join(addresses_l['CC'])
328 if addresses_l['Reply-To']:
329 payload_part['Reply-To'] = addresses_l['Reply-To'][0]
331 payload_part['Message-ID'] = message_id
332 if body.get('text', False):
333 temp_body_text = body.get('text', '')
334 l = len(temp_body_text.replace(' ', '').replace('\r', '').replace('\n', ''))
336 body['text'] = u'No Mail Message'
337 # Attach parts into message container.
338 # According to RFC 2046, the last part of a multipart message, in this case
339 # the HTML message, is best and preferred.
340 if core_obj.send_pref in ('text', 'mixed', 'alternative'):
341 body_text = body.get('text', u'<Empty Message>')
342 body_text = tools.ustr(body_text)
343 text_part.attach(MIMEText(body_text.encode("utf-8"), _charset='UTF-8'))
344 if core_obj.send_pref in ('html', 'mixed', 'alternative'):
345 html_body = body.get('html', u'')
346 if len(html_body) == 0 or html_body == u'':
347 html_body = body.get('text', u'<p><Empty Message></p>').replace('\n', '<br/>').replace('\r', '<br/>')
348 html_body = tools.ustr(html_body)
349 text_part.attach(MIMEText(html_body.encode("utf-8"), _subtype='html', _charset='UTF-8'))
351 #Now add attachments if any, wrapping into a container multipart/mixed if needed
354 part = MIMEBase('application', "octet-stream")
355 part.set_payload(base64.decodestring(payload[file]))
356 part.add_header('Content-Disposition', 'attachment; filename="%s"' % file)
357 Encoders.encode_base64(part)
358 payload_part.attach(part)
359 except Exception, error:
360 logger.notifyChannel(_("Email Template"), netsvc.LOG_ERROR, _("Mail from Account %s failed. Probable Reason:MIME Error\nDescription: %s") % (id, error))
361 return {'error_msg': "Server Send Error\nDescription: %s"%error}
363 serv.sendmail(payload_part['From'], addresses_l['all-recipients'], payload_part.as_string())
364 except Exception, error:
365 logging.getLogger('email_template').error("Mail from Account %s failed. Probable Reason: Server Send Error\n Description: %s", id, error, exc_info=True)
366 return {'error_msg': "Server Send Error\nDescription: %s"%error}
367 #The mail sending is complete
369 logger.notifyChannel(_("Email Template"), netsvc.LOG_INFO, _("Mail from Account %s successfully Sent.") % (id))
372 logger.notifyChannel(_("Email Template"), netsvc.LOG_ERROR, _("Mail from Account %s failed. Probable Reason:Account not approved") % id)
373 return {'error_msg':"Mail from Account %s failed. Probable Reason:Account not approved"% id}
375 def extracttime(self, time_as_string):
379 logger = netsvc.Logger()
380 #The standard email dates are of format similar to:
381 #Thu, 8 Oct 2009 09:35:42 +0200
383 convertor = {'+':1, '-':-1}
385 time_as_string = time_as_string.replace(',', '')
386 date_list = time_as_string.split(' ')
387 date_temp_str = ' '.join(date_list[1:5])
388 if len(date_list) >= 6:
389 sign = convertor.get(date_list[5][0], False)
393 dt = datetime.datetime.strptime(
398 dt = datetime.datetime.strptime(
405 offset = datetime.timedelta(
413 except Exception, e2:
414 """Looks like UT or GMT, just forget decoding"""
417 offset = datetime.timedelta(hours=0)
419 date_as_date = dt.strftime('%Y-%m-%d %H:%M:%S')
421 logger.notifyChannel(
425 "Datetime Extraction failed.Date:%s \
432 def send_receive(self, cr, uid, ids, context=None):
435 ctx['filters'] = [('account_id', '=', id)]
436 self.pool.get('email_template.mailbox').send_all_mail(cr, uid, [], context=ctx)
439 def decode_header_text(self, text):
440 """ Decode internationalized headers RFC2822.
441 To, CC, BCC, Subject fields can contain
442 text slices with different encodes, like:
443 =?iso-8859-1?Q?Enric_Mart=ED?= <enricmarti@company.com>,
444 =?Windows-1252?Q?David_G=F3mez?= <david@company.com>
445 Sometimes they include extra " character at the beginning/
446 end of the contact name, like:
447 "=?iso-8859-1?Q?Enric_Mart=ED?=" <enricmarti@company.com>
448 and decode_header() does not work well, so we use regular
449 expressions (?= ? ? ?=) to split the text slices
453 p = re.compile("(=\?.*?\?.\?.*?\?=)")
456 for t2 in p.split(text):
460 ) for (s, t) in decode_header(t2)]
466 email_template_account()
468 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: