Launchpad automatic translations update.
[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         #Type cast ids to integer
213         if type(ids) == list:
214             ids = ids[0]
215         this_object = self.browse(cursor, user, ids, context=context)
216         if this_object:
217             if this_object.smtpserver and this_object.smtpport: 
218                 try:
219                     if this_object.smtpssl:
220                         serv = smtplib.SMTP_SSL(this_object.smtpserver, this_object.smtpport)
221                     else:
222                         serv = smtplib.SMTP(this_object.smtpserver, this_object.smtpport)
223                     if this_object.smtptls:
224                         serv.ehlo()
225                         serv.starttls()
226                         serv.ehlo()
227                 except Exception, error:
228                     raise error
229                 try:
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:
233                     raise error
234                 return serv
235             raise Exception(_("SMTP SERVER or PORT not specified"))
236         raise Exception(_("Core connection for the given ID does not exist"))
237     
238     def check_outgoing_connection(self, cursor, user, ids, context=None):
239         """
240         checks SMTP credentials and confirms if outgoing connection works
241         (Attached to button)
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
247         """
248         try:
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") % error
257                                  )
258     
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")
263
264     def smtp_connection(self, cursor, user, id, context=None):
265         """
266         This method should now wrap smtp_connection
267         """
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':
272             try:
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, error))
276                 return False
277             #Everything is complete, now return the connection
278             return serv
279         else:
280             logger.notifyChannel(_("Email Template"), netsvc.LOG_ERROR, _("Mail from Account %s failed. Probable Reason:Account not approved") % id)
281             return False
282                       
283 #**************************** MAIL SENDING FEATURES ***********************#
284
285
286
287     
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
290         if body is None:
291             body = {}
292         if payload is None:
293             payload = {}
294         if context is None:
295             context = {}
296         logger = netsvc.Logger()
297         for id in ids:  
298             core_obj = self.browse(cr, uid, id, context)
299             serv = self.smtp_connection(cr, uid, id)
300             if serv:
301                 try:
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)
309                     else:
310                         # otherwise a single multipart/mixed will do the whole job 
311                         payload_part = text_part = MIMEMultipart(_subtype=text_subtype)
312
313                     if subject:
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()
319                     else:
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]
332                     if message_id:
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', ''))
337                         if l == 0:
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>&lt;Empty Message&gt;</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'))
352
353                     #Now add attachments if any, wrapping into a container multipart/mixed if needed
354                     if payload:
355                         for file in payload:
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, error))
363                     return {'error_msg': _("Server Send Error\nDescription: %s")%error}
364                 try:
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, error, exc_info=True)
368                     return {'error_msg': _("Server Send Error\nDescription: %s")%error}
369                 #The mail sending is complete
370                 serv.close()
371                 logger.notifyChannel(_("Email Template"), netsvc.LOG_INFO, _("Mail from Account %s successfully Sent.") % (id))
372                 return True
373             else:
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}
376
377     def extracttime(self, time_as_string):
378         """
379         TODO: DOC THis
380         """
381         logger = netsvc.Logger()
382         #The standard email dates are of format similar to:
383         #Thu, 8 Oct 2009 09:35:42 +0200
384         date_as_date = False
385         convertor = {'+':1, '-':-1}
386         try:
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)
392             else:
393                 sign = False
394             try:
395                 dt = datetime.datetime.strptime(
396                                             date_temp_str,
397                                             "%d %b %Y %H:%M:%S")
398             except:
399                 try:
400                     dt = datetime.datetime.strptime(
401                                             date_temp_str,
402                                             "%d %b %Y %H:%M")
403                 except:
404                     return False
405             if sign:
406                 try:
407                     offset = datetime.timedelta(
408                                 hours=sign * int(
409                                              date_list[5][1:3]
410                                                 ),
411                                              minutes=sign * int(
412                                                             date_list[5][3:5]
413                                                                 )
414                                                 )
415                 except Exception, e2:
416                     """Looks like UT or GMT, just forget decoding"""
417                     return False
418             else:
419                 offset = datetime.timedelta(hours=0)
420             dt = dt + offset
421             date_as_date = dt.strftime('%Y-%m-%d %H:%M:%S')
422         except Exception, e:
423             logger.notifyChannel(
424                     _("Email Template"),
425                     netsvc.LOG_WARNING,
426                     _(
427                       "Datetime Extraction failed.Date:%s \
428                       \tError:%s") % (
429                                     time_as_string,
430                                     e)
431                       )
432         return date_as_date
433         
434     def send_receive(self, cr, uid, ids, context=None):
435         for id in ids:
436             ctx = context.copy()
437             ctx['filters'] = [('account_id', '=', id)]
438             self.pool.get('email_template.mailbox').send_all_mail(cr, uid, [], context=ctx)
439         return True
440  
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
452         """
453         if not text:
454             return text        
455         p = re.compile("(=\?.*?\?.\?.*?\?=)")
456         text2 = ''
457         try:
458             for t2 in p.split(text):
459                 text2 += ''.join(
460                             [s.decode(
461                                       t or 'ascii'
462                                     ) for (s, t) in decode_header(t2)]
463                                 ).encode('utf-8')
464         except:
465             return text
466         return text2
467
468 email_template_account()
469
470 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: