[IMP] : Added context=None on methods used for _constraints and replaced context...
[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 import logging
38
39 EMAIL_PATTERN = re.compile(r'([^()\[\] ,<:\\>@";]+@[^()\[\] ,<:\\>@";]+)') # See RFC822
40 def extract_emails(emails_str):
41     """
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']"
44     """
45     return EMAIL_PATTERN.findall(emails_str)
46
47
48 def extract_emails_from_dict(addresses={}):
49     """
50     Extracts email addresses from a dictionary with comma-separated address string values, handling
51     separately the To, CC, BCC and Reply-To addresses.
52
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.
56     """
57     result = {'all-recipients':[]}
58     keys = ['To', 'CC', 'BCC', 'Reply-To']
59     for each in keys:
60         emails = extract_emails(addresses.get(each, u''))
61         while u'' in emails:
62             emails.remove(u'')
63         result[each] = emails
64         if each != 'Reply-To':
65             result['all-recipients'].extend(emails)
66     return result
67
68 class email_template_account(osv.osv):
69     """
70     Object to store email account settings
71     """
72     _name = "email_template.account"
73     _known_content_types = ['multipart/mixed',
74                             'multipart/alternative',
75                             'multipart/related',
76                             'text/plain',
77                             'text/html'
78                             ]
79     _columns = {
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),
115                                 
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([
125                         ('yes', 'Yes'),
126                         ('no', 'No')
127                         ], 'Corporate',
128                         readonly=True,
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)]
134                                            }),
135
136         'state':fields.selection([
137                                   ('draft', 'Initiated'),
138                                   ('suspended', 'Suspended'),
139                                   ('approved', 'Approved')
140                                   ],
141                         'State', required=True, readonly=True),
142     }
143
144     _defaults = {
145          'name':lambda self, cursor, user, context:self.pool.get(
146                                                 'res.users'
147                                                 ).read(
148                                                         cursor,
149                                                         user,
150                                                         user,
151                                                         ['name'],
152                                                         context
153                                                         )['name'],
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,
161      }
162     
163     _sql_constraints = [
164         (
165          'email_uniq',
166          'unique (email_id)',
167          'Another setting already exists with this email ID !')
168     ]
169
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)]
172
173     def _constraint_unique(self, cursor, user, ids, context=None):
174         """
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
179         """
180         if self.read(cursor, user, ids, ['company'])[0]['company'] == 'no':
181             accounts = self.search(cursor, user, [
182                                                  ('user', '=', user),
183                                                  ('company', '=', 'no')
184                                                  ])
185             if len(accounts) > 1 :
186                 return False
187             else :
188                 return True
189         else:
190             return True
191         
192     _constraints = [
193         (_constraint_unique,
194          'Error: You are not allowed to have more than 1 account.',
195          [])
196     ]
197
198     def get_outgoing_server(self, cursor, user, ids, context=None):
199         """
200         Returns the Out Going Connection (SMTP) object
201         
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
209         
210         @return: SMTP server object or Exception
211         """
212         if not context:
213             context = {}
214         #Type cast ids to integer
215         if type(ids) == list:
216             ids = ids[0]
217         this_object = self.browse(cursor, user, ids, context=context)
218         if this_object:
219             if this_object.smtpserver and this_object.smtpport: 
220                 try:
221                     if this_object.smtpssl:
222                         serv = smtplib.SMTP_SSL(this_object.smtpserver, this_object.smtpport)
223                     else:
224                         serv = smtplib.SMTP(this_object.smtpserver, this_object.smtpport)
225                     if this_object.smtptls:
226                         serv.ehlo()
227                         serv.starttls()
228                         serv.ehlo()
229                 except Exception, error:
230                     raise error
231                 try:
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:
235                     raise error
236                 return serv
237             raise Exception(_("SMTP SERVER or PORT not specified"))
238         raise Exception(_("Core connection for the given ID does not exist"))
239     
240     def check_outgoing_connection(self, cursor, user, ids, context=None):
241         """
242         checks SMTP credentials and confirms if outgoing connection works
243         (Attached to button)
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
249         """
250         try:
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
259                                  )
260     
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")
265
266     def smtp_connection(self, cursor, user, id, context=None):
267         """
268         This method should now wrap smtp_connection
269         """
270         #This function returns a SMTP server object
271         if not context:
272             context = {}
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':
276             try:
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))
280                 return False
281             #Everything is complete, now return the connection
282             return serv
283         else:
284             logger.notifyChannel(_("Email Template"), netsvc.LOG_ERROR, _("Mail from Account %s failed. Probable Reason:Account not approved") % id)
285             return False
286                       
287 #**************************** MAIL SENDING FEATURES ***********************#
288
289
290
291     
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
294         if body is None:
295             body = {}
296         if payload is None:
297             payload = {}
298         if context is None:
299             context = {}
300         logger = netsvc.Logger()
301         for id in ids:  
302             core_obj = self.browse(cr, uid, id, context)
303             serv = self.smtp_connection(cr, uid, id)
304             if serv:
305                 try:
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)
313                     else:
314                         # otherwise a single multipart/mixed will do the whole job 
315                         payload_part = text_part = MIMEMultipart(_subtype=text_subtype)
316
317                     if subject:
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()
323                     else:
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]
336                     if message_id:
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', ''))
341                         if l == 0:
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>&lt;Empty Message&gt;</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'))
356
357                     #Now add attachments if any, wrapping into a container multipart/mixed if needed
358                     if payload:
359                         for file in payload:
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}
368                 try:
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
374                 serv.close()
375                 logger.notifyChannel(_("Email Template"), netsvc.LOG_INFO, _("Mail from Account %s successfully Sent.") % (id))
376                 return True
377             else:
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}
380
381     def extracttime(self, time_as_string):
382         """
383         TODO: DOC THis
384         """
385         logger = netsvc.Logger()
386         #The standard email dates are of format similar to:
387         #Thu, 8 Oct 2009 09:35:42 +0200
388         date_as_date = False
389         convertor = {'+':1, '-':-1}
390         try:
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)
396             else:
397                 sign = False
398             try:
399                 dt = datetime.datetime.strptime(
400                                             date_temp_str,
401                                             "%d %b %Y %H:%M:%S")
402             except:
403                 try:
404                     dt = datetime.datetime.strptime(
405                                             date_temp_str,
406                                             "%d %b %Y %H:%M")
407                 except:
408                     return False
409             if sign:
410                 try:
411                     offset = datetime.timedelta(
412                                 hours=sign * int(
413                                              date_list[5][1:3]
414                                                 ),
415                                              minutes=sign * int(
416                                                             date_list[5][3:5]
417                                                                 )
418                                                 )
419                 except Exception, e2:
420                     """Looks like UT or GMT, just forget decoding"""
421                     return False
422             else:
423                 offset = datetime.timedelta(hours=0)
424             dt = dt + offset
425             date_as_date = dt.strftime('%Y-%m-%d %H:%M:%S')
426         except Exception, e:
427             logger.notifyChannel(
428                     _("Email Template"),
429                     netsvc.LOG_WARNING,
430                     _(
431                       "Datetime Extraction failed.Date:%s \
432                       \tError:%s") % (
433                                     time_as_string,
434                                     e)
435                       )
436         return date_as_date
437         
438     def send_receive(self, cr, uid, ids, context=None):
439         for id in ids:
440             ctx = context.copy()
441             ctx['filters'] = [('account_id', '=', id)]
442             self.pool.get('email_template.mailbox').send_all_mail(cr, uid, [], context=ctx)
443         return True
444  
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
456         """
457         if not text:
458             return text        
459         p = re.compile("(=\?.*?\?.\?.*?\?=)")
460         text2 = ''
461         try:
462             for t2 in p.split(text):
463                 text2 += ''.join(
464                             [s.decode(
465                                       t or 'ascii'
466                                     ) for (s, t) in decode_header(t2)]
467                                 ).encode('utf-8')
468         except:
469             return text
470         return text2
471
472 email_template_account()
473
474 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: