[IMP] email_template: review + many improvements:
[odoo/odoo.git] / addons / email_template / email_template_account.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2009 Sharoon Thomas
6 #    Copyright (C) 2004-2010 OpenERP SA (<http://www.openerp.com>)
7 #
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.
12 #
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.
17 #
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/>
20 #
21 ##############################################################################
22
23 from osv import osv, fields
24 import re
25 import smtplib
26 import base64
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
33 import netsvc
34 import datetime
35 from tools.translate import _
36 import tools
37
38 class email_template_account(osv.osv):
39     """
40     Object to store email account settings
41     """
42     _name = "email_template.account"
43     _known_content_types = ['multipart/mixed',
44                             'multipart/alternative',
45                             'multipart/related',
46                             'text/plain',
47                             'text/html'
48                             ]
49     _columns = {
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),
80                                 
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([
90                         ('yes', 'Yes'),
91                         ('no', 'No')
92                         ], 'Corporate',
93                         readonly=True,
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)]
99                                            }),
100
101         'state':fields.selection([
102                                   ('draft', 'Initiated'),
103                                   ('suspended', 'Suspended'),
104                                   ('approved', 'Approved')
105                                   ],
106                         'Status', required=True, readonly=True),
107     }
108
109     _defaults = {
110          'name':lambda self, cursor, user, context:self.pool.get(
111                                                 'res.users'
112                                                 ).read(
113                                                         cursor,
114                                                         user,
115                                                         user,
116                                                         ['name'],
117                                                         context
118                                                         )['name'],
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,
126      }
127     
128     _sql_constraints = [
129         (
130          'email_uniq',
131          'unique (email_id)',
132          'Another setting already exists with this email ID !')
133     ]
134
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)])
137
138     def _constraint_unique(self, cursor, user, ids):
139         """
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
144         """
145         if self.read(cursor, user, ids, ['company'])[0]['company'] == 'no':
146             accounts = self.search(cursor, user, [
147                                                  ('user', '=', user),
148                                                  ('company', '=', 'no')
149                                                  ])
150             if len(accounts) > 1 :
151                 return False
152             else :
153                 return True
154         else:
155             return True
156         
157     _constraints = [
158         (_constraint_unique,
159          'Error: You are not allowed to have more than 1 account.',
160          [])
161     ]
162     
163     def on_change_emailid(self, cursor, user, ids, name=None, email_id=None, context=None):
164         """
165         Called when the email ID field changes.
166         
167         UI enhancement
168         Writes the same email value to the smtpusername
169         and incoming username
170         """
171         #TODO: Check and remove the write. Is it needed?
172         self.write(cursor, user, ids, {'state':'draft'}, context=context)
173         return {
174                 'value': {
175                           'state': 'draft',
176                           'smtpuname':email_id,
177                           'isuser':email_id
178                           }
179                 }
180     
181     def get_outgoing_server(self, cursor, user, ids, context=None):
182         """
183         Returns the Out Going Connection (SMTP) object
184         
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
192         
193         @return: SMTP server object or Exception
194         """
195         #Type cast ids to integer
196         if type(ids) == list:
197             ids = ids[0]
198         this_object = self.browse(cursor, user, ids, context)
199         if this_object:
200             if this_object.smtpserver and this_object.smtpport: 
201                 try:
202                     if this_object.smtpssl:
203                         serv = smtplib.SMTP_SSL(this_object.smtpserver, this_object.smtpport)
204                     else:
205                         serv = smtplib.SMTP(this_object.smtpserver, this_object.smtpport)
206                     if this_object.smtptls:
207                         serv.ehlo()
208                         serv.starttls()
209                         serv.ehlo()
210                 except Exception, error:
211                     raise error
212                 try:
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:
216                     raise error
217                 return serv
218             raise Exception(_("SMTP SERVER or PORT not specified"))
219         raise Exception(_("Core connection for the given ID does not exist"))
220     
221     def check_outgoing_connection(self, cursor, user, ids, context=None):
222         """
223         checks SMTP credentials and confirms if outgoing connection works
224         (Attached to button)
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
230         """
231         try:
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
240                                  )
241     
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")
246
247     def smtp_connection(self, cursor, user, id, context=None):
248         """
249         This method should now wrap smtp_connection
250         """
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':
255             try:
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))
259                 return False
260             #Everything is complete, now return the connection
261             return serv
262         else:
263             logger.notifyChannel(_("Email Template"), netsvc.LOG_ERROR, _("Mail from Account %s failed. Probable Reason:Account not approved") % id)
264             return False
265                       
266 #**************************** MAIL SENDING FEATURES ***********************#
267     def split_to_ids(self, ids_as_str):
268         """
269         Identifies email IDs separated by separators
270         and returns a list
271         TODO: Doc this
272         "a@b.com,c@bcom; d@b.com;e@b.com->['a@b.com',...]"
273         """
274         email_sep_by_commas = ids_as_str \
275                                     .replace('; ', ',') \
276                                     .replace(';', ',') \
277                                     .replace(', ', ',')
278         return email_sep_by_commas.split(',')
279     
280     def get_ids_from_dict(self, addresses={}):
281         """
282         TODO: Doc this
283         """
284         result = {'all':[]}
285         keys = ['To', 'CC', 'BCC', 'Reply-To']
286         for each in keys:
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)
292         return result
293     
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
296         if body is None:
297             body = {}
298         if payload is None:
299             payload = {}
300         if context is None:
301             context = {}
302         logger = netsvc.Logger()
303         for id in ids:  
304             core_obj = self.browse(cr, uid, id, context)
305             serv = self.smtp_connection(cr, uid, id)
306             if serv:
307                 try:
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)
315                     else:
316                         # otherwise a single multipart/mixed will do the whole job 
317                         payload_part = text_part = MIMEMultipart(_subtype=text_subtype)
318
319                     if subject:
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()
325                     else:
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]
338                     if message_id:
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', ''))
343                         if l == 0:
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>&lt;Empty Message&gt;</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'))
358
359                     #Now add attachments if any, wrapping into a container multipart/mixed if needed
360                     if payload:
361                         for file in payload:
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}
370                 try:
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
376                 serv.close()
377                 logger.notifyChannel(_("Email Template"), netsvc.LOG_INFO, _("Mail from Account %s successfully Sent.") % (id))
378                 return True
379             else:
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}
382
383     def extracttime(self, time_as_string):
384         """
385         TODO: DOC THis
386         """
387         logger = netsvc.Logger()
388         #The standard email dates are of format similar to:
389         #Thu, 8 Oct 2009 09:35:42 +0200
390         date_as_date = False
391         convertor = {'+':1, '-':-1}
392         try:
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)
398             else:
399                 sign = False
400             try:
401                 dt = datetime.datetime.strptime(
402                                             date_temp_str,
403                                             "%d %b %Y %H:%M:%S")
404             except:
405                 try:
406                     dt = datetime.datetime.strptime(
407                                             date_temp_str,
408                                             "%d %b %Y %H:%M")
409                 except:
410                     return False
411             if sign:
412                 try:
413                     offset = datetime.timedelta(
414                                 hours=sign * int(
415                                              date_list[5][1:3]
416                                                 ),
417                                              minutes=sign * int(
418                                                             date_list[5][3:5]
419                                                                 )
420                                                 )
421                 except Exception, e2:
422                     """Looks like UT or GMT, just forget decoding"""
423                     return False
424             else:
425                 offset = datetime.timedelta(hours=0)
426             dt = dt + offset
427             date_as_date = dt.strftime('%Y-%m-%d %H:%M:%S')
428         except Exception, e:
429             logger.notifyChannel(
430                     _("Email Template"),
431                     netsvc.LOG_WARNING,
432                     _(
433                       "Datetime Extraction failed.Date:%s \
434                       \tError:%s") % (
435                                     time_as_string,
436                                     e)
437                       )
438         return date_as_date
439         
440     def send_receive(self, cr, uid, ids, context=None):
441         for id in ids:
442             ctx = context.copy()
443             ctx['filters'] = [('account_id', '=', id)]
444             self.pool.get('email_template.mailbox').send_all_mail(cr, uid, [], context=ctx)
445         return True
446  
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
458         """
459         if not text:
460             return text        
461         p = re.compile("(=\?.*?\?.\?.*?\?=)")
462         text2 = ''
463         try:
464             for t2 in p.split(text):
465                 text2 += ''.join(
466                             [s.decode(
467                                       t or 'ascii'
468                                     ) for (s, t) in decode_header(t2)]
469                                 ).encode('utf-8')
470         except:
471             return text
472         return text2
473
474 email_template_account()
475
476 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: