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 _
38 class email_template_account(osv.osv):
40 Object to store email account settings
42 _name = "email_template.account"
43 _known_content_types = ['multipart/mixed',
44 'multipart/alternative',
50 'name': fields.char('Description',
51 size=64, required=True,
52 readonly=True, select=True,
53 states={'draft':[('readonly', False)]}),
54 'user':fields.many2one('res.users',
55 'Related User', required=True,
56 readonly=True, states={'draft':[('readonly', False)]}),
57 'email_id': fields.char('From Email',
58 size=120, required=True,
59 readonly=True, states={'draft':[('readonly', False)]} ,
60 help="eg: yourname@yourdomain.com "),
61 'smtpserver': fields.char('Server',
62 size=120, required=True,
63 readonly=True, states={'draft':[('readonly', False)]},
64 help="Enter name of outgoing server, eg:smtp.gmail.com "),
65 'smtpport': fields.integer('SMTP Port ',
66 size=64, required=True,
67 readonly=True, states={'draft':[('readonly', False)]},
68 help="Enter port number,eg:SMTP-587 "),
69 'smtpuname': fields.char('User Name',
70 size=120, required=False,
71 readonly=True, states={'draft':[('readonly', False)]},
72 help="Specify the username if your SMTP server requires authentication, "
73 "otherwise leave it empty."),
74 'smtppass': fields.char('Password',
75 size=120, invisible=True,
76 required=False, readonly=True,
77 states={'draft':[('readonly', False)]}),
78 'smtptls':fields.boolean('TLS',
79 states={'draft':[('readonly', False)]}, readonly=True),
81 'smtpssl':fields.boolean('SSL/TLS (only in python 2.6)',
82 states={'draft':[('readonly', False)]}, readonly=True),
83 'send_pref':fields.selection([
84 ('html', 'HTML, otherwise Text'),
85 ('text', 'Text, otherwise HTML'),
86 ('alternative', 'Both HTML & Text (Alternative)'),
87 ('mixed', 'Both HTML & Text (Mixed)')
88 ], 'Mail Format', required=True),
89 'company':fields.selection([
94 help="Select if this mail account does not belong " \
95 "to specific user but to the organization as a whole. " \
96 "eg: info@companydomain.com",
97 required=True, states={
98 'draft':[('readonly', False)]
101 'state':fields.selection([
102 ('draft', 'Initiated'),
103 ('suspended', 'Suspended'),
104 ('approved', 'Approved')
106 'Status', required=True, readonly=True),
110 'name':lambda self, cursor, user, context:self.pool.get(
119 'state':lambda * a:'draft',
120 'smtpport':lambda *a:25,
121 'smtpserver':lambda *a:'localhost',
122 'company':lambda *a:'yes',
123 'user':lambda self, cursor, user, context:user,
124 'send_pref':lambda *a: 'html',
125 'smtptls':lambda *a:True,
132 'Another setting already exists with this email ID !')
135 def name_get(self, cr, uid, ids, context=None):
136 return dict([(a["id"], "%s (%s)" % (a['email_id'], a['name'])) for a in self.read(cr, uid, ids, ['name', 'email_id'], context=context)])
138 def _constraint_unique(self, cursor, user, ids):
140 This makes sure that you dont give personal
141 users two accounts with same ID (Validated in sql constaints)
142 However this constraint exempts company accounts.
143 Any no of co accounts for a user is allowed
145 if self.read(cursor, user, ids, ['company'])[0]['company'] == 'no':
146 accounts = self.search(cursor, user, [
148 ('company', '=', 'no')
150 if len(accounts) > 1 :
159 'Error: You are not allowed to have more than 1 account.',
163 def on_change_emailid(self, cursor, user, ids, name=None, email_id=None, context=None):
165 Called when the email ID field changes.
168 Writes the same email value to the smtpusername
169 and incoming username
171 #TODO: Check and remove the write. Is it needed?
172 self.write(cursor, user, ids, {'state':'draft'}, context=context)
176 'smtpuname':email_id,
181 def get_outgoing_server(self, cursor, user, ids, context=None):
183 Returns the Out Going Connection (SMTP) object
185 @attention: DO NOT USE except_osv IN THIS METHOD
186 @param cursor: Database Cursor
187 @param user: ID of current user
188 @param ids: ID/list of ids of current object for
189 which connection is required
190 First ID will be chosen from lists
191 @param context: Context
193 @return: SMTP server object or Exception
195 #Type cast ids to integer
196 if type(ids) == list:
198 this_object = self.browse(cursor, user, ids, context)
200 if this_object.smtpserver and this_object.smtpport:
202 if this_object.smtpssl:
203 serv = smtplib.SMTP_SSL(this_object.smtpserver, this_object.smtpport)
205 serv = smtplib.SMTP(this_object.smtpserver, this_object.smtpport)
206 if this_object.smtptls:
210 except Exception, error:
213 if serv.has_extn('AUTH') or this_object.smtpuname or this_object.smtppass:
214 serv.login(this_object.smtpuname, this_object.smtppass)
215 except Exception, error:
218 raise Exception(_("SMTP SERVER or PORT not specified"))
219 raise Exception(_("Core connection for the given ID does not exist"))
221 def check_outgoing_connection(self, cursor, user, ids, context=None):
223 checks SMTP credentials and confirms if outgoing connection works
225 @param cursor: Database Cursor
226 @param user: ID of current user
227 @param ids: list of ids of current object for
228 which connection is required
229 @param context: Context
232 self.get_outgoing_server(cursor, user, ids, context)
233 raise osv.except_osv(_("SMTP Test Connection Was Successful"), '')
234 except osv.except_osv, success_message:
235 raise success_message
236 except Exception, error:
237 raise osv.except_osv(
238 _("Out going connection test failed"),
239 _("Reason: %s") % error
242 def do_approval(self, cr, uid, ids, context={}):
243 #TODO: Check if user has rights
244 self.write(cr, uid, ids, {'state':'approved'}, context=context)
245 # wf_service = netsvc.LocalService("workflow")
247 def smtp_connection(self, cursor, user, id, context=None):
249 This method should now wrap smtp_connection
251 #This function returns a SMTP server object
252 logger = netsvc.Logger()
253 core_obj = self.browse(cursor, user, id, context)
254 if core_obj.smtpserver and core_obj.smtpport and core_obj.state == 'approved':
256 serv = self.get_outgoing_server(cursor, user, id, context)
257 except Exception, error:
258 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))
260 #Everything is complete, now return the connection
263 logger.notifyChannel(_("Email Template"), netsvc.LOG_ERROR, _("Mail from Account %s failed. Probable Reason:Account not approved") % id)
266 #**************************** MAIL SENDING FEATURES ***********************#
267 def split_to_ids(self, ids_as_str):
269 Identifies email IDs separated by separators
272 "a@b.com,c@bcom; d@b.com;e@b.com->['a@b.com',...]"
274 email_sep_by_commas = ids_as_str \
275 .replace('; ', ',') \
278 return email_sep_by_commas.split(',')
280 def get_ids_from_dict(self, addresses={}):
285 keys = ['To', 'CC', 'BCC', 'Reply-To']
287 ids_as_list = self.split_to_ids(addresses.get(each, u''))
288 while u'' in ids_as_list:
289 ids_as_list.remove(u'')
290 result[each] = ids_as_list
291 result['all'].extend(ids_as_list)
294 def send_mail(self, cr, uid, ids, addresses, subject='', body=None, payload=None, message_id=None, context=None):
295 #TODO: Replace all this with a single email object
302 logger = netsvc.Logger()
304 core_obj = self.browse(cr, uid, id, context)
305 serv = self.smtp_connection(cr, uid, id)
308 # Prepare multipart containers depending on data
309 text_subtype = (core_obj.send_pref == 'alternative') and 'alternative' or 'mixed'
310 # Need a multipart/mixed wrapper for attachments if content is alternative
311 if payload and text_subtype == 'alternative':
312 payload_part = MIMEMultipart(_subtype='mixed')
313 text_part = MIMEMultipart(_subtype=text_subtype)
314 payload_part.attach(text_part)
316 # otherwise a single multipart/mixed will do the whole job
317 payload_part = text_part = MIMEMultipart(_subtype=text_subtype)
320 payload_part['Subject'] = subject
321 from_email = core_obj.email_id
322 if '<' in from_email:
323 # We have a structured email address, keep it untouched
324 payload_part['From'] = Header(core_obj.email_id, 'utf-8').encode()
326 # Plain email address, construct a structured one based on the name:
327 sender_name = Header(core_obj.name, 'utf-8').encode()
328 payload_part['From'] = sender_name + " <" + core_obj.email_id + ">"
329 payload_part['Organization'] = tools.ustr(core_obj.user.company_id.name)
330 payload_part['Date'] = formatdate()
331 addresses_l = self.get_ids_from_dict(addresses)
332 if addresses_l['To']:
333 payload_part['To'] = u','.join(addresses_l['To'])
334 if addresses_l['CC']:
335 payload_part['CC'] = u','.join(addresses_l['CC'])
336 if addresses_l['Reply-To']:
337 payload_part['Reply-To'] = addresses_l['Reply-To'][0]
339 payload_part['Message-ID'] = message_id
340 if body.get('text', False):
341 temp_body_text = body.get('text', '')
342 l = len(temp_body_text.replace(' ', '').replace('\r', '').replace('\n', ''))
344 body['text'] = u'No Mail Message'
345 # Attach parts into message container.
346 # According to RFC 2046, the last part of a multipart message, in this case
347 # the HTML message, is best and preferred.
348 if core_obj.send_pref in ('text', 'mixed', 'alternative'):
349 body_text = body.get('text', u'<Empty Message>')
350 body_text = tools.ustr(body_text)
351 text_part.attach(MIMEText(body_text.encode("utf-8"), _charset='UTF-8'))
352 if core_obj.send_pref in ('html', 'mixed', 'alternative'):
353 html_body = body.get('html', u'')
354 if len(html_body) == 0 or html_body == u'':
355 html_body = body.get('text', u'<p><Empty Message></p>').replace('\n', '<br/>').replace('\r', '<br/>')
356 html_body = tools.ustr(html_body)
357 text_part.attach(MIMEText(html_body.encode("utf-8"), _subtype='html', _charset='UTF-8'))
359 #Now add attachments if any, wrapping into a container multipart/mixed if needed
362 part = MIMEBase('application', "octet-stream")
363 part.set_payload(base64.decodestring(payload[file]))
364 part.add_header('Content-Disposition', 'attachment; filename="%s"' % file)
365 Encoders.encode_base64(part)
366 payload_part.attach(part)
367 except Exception, error:
368 logger.notifyChannel(_("Email Template"), netsvc.LOG_ERROR, _("Mail from Account %s failed. Probable Reason:MIME Error\nDescription: %s") % (id, error))
369 return {'error_msg': "Server Send Error\nDescription: %s"%error}
371 serv.sendmail(payload_part['From'], addresses_l['all'], payload_part.as_string())
372 except Exception, error:
373 logger.notifyChannel(_("Email Template"), netsvc.LOG_ERROR, _("Mail from Account %s failed. Probable Reason:Server Send Error\nDescription: %s") % (id, error))
374 return {'error_msg': "Server Send Error\nDescription: %s"%error}
375 #The mail sending is complete
377 logger.notifyChannel(_("Email Template"), netsvc.LOG_INFO, _("Mail from Account %s successfully Sent.") % (id))
380 logger.notifyChannel(_("Email Template"), netsvc.LOG_ERROR, _("Mail from Account %s failed. Probable Reason:Account not approved") % id)
381 return {'error_msg':"Mail from Account %s failed. Probable Reason:Account not approved"% id}
383 def extracttime(self, time_as_string):
387 logger = netsvc.Logger()
388 #The standard email dates are of format similar to:
389 #Thu, 8 Oct 2009 09:35:42 +0200
391 convertor = {'+':1, '-':-1}
393 time_as_string = time_as_string.replace(',', '')
394 date_list = time_as_string.split(' ')
395 date_temp_str = ' '.join(date_list[1:5])
396 if len(date_list) >= 6:
397 sign = convertor.get(date_list[5][0], False)
401 dt = datetime.datetime.strptime(
406 dt = datetime.datetime.strptime(
413 offset = datetime.timedelta(
421 except Exception, e2:
422 """Looks like UT or GMT, just forget decoding"""
425 offset = datetime.timedelta(hours=0)
427 date_as_date = dt.strftime('%Y-%m-%d %H:%M:%S')
429 logger.notifyChannel(
433 "Datetime Extraction failed.Date:%s \
440 def send_receive(self, cr, uid, ids, context=None):
443 ctx['filters'] = [('account_id', '=', id)]
444 self.pool.get('email_template.mailbox').send_all_mail(cr, uid, [], context=ctx)
447 def decode_header_text(self, text):
448 """ Decode internationalized headers RFC2822.
449 To, CC, BCC, Subject fields can contain
450 text slices with different encodes, like:
451 =?iso-8859-1?Q?Enric_Mart=ED?= <enricmarti@company.com>,
452 =?Windows-1252?Q?David_G=F3mez?= <david@company.com>
453 Sometimes they include extra " character at the beginning/
454 end of the contact name, like:
455 "=?iso-8859-1?Q?Enric_Mart=ED?=" <enricmarti@company.com>
456 and decode_header() does not work well, so we use regular
457 expressions (?= ? ? ?=) to split the text slices
461 p = re.compile("(=\?.*?\?.\?.*?\?=)")
464 for t2 in p.split(text):
468 ) for (s, t) in decode_header(t2)]
474 email_template_account()
476 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: