[ADD]: email_template: Added a boolean field "auto_delete" on email account which...
[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                         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),
113                                 
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([
123                         ('yes', 'Yes'),
124                         ('no', 'No')
125                         ], 'Corporate',
126                         readonly=True,
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)]
132                                            }),
133
134         'state':fields.selection([
135                                   ('draft', 'Initiated'),
136                                   ('suspended', 'Suspended'),
137                                   ('approved', 'Approved')
138                                   ],
139                         'Status', required=True, readonly=True),
140     }
141
142     _defaults = {
143          'name':lambda self, cursor, user, context:self.pool.get(
144                                                 'res.users'
145                                                 ).read(
146                                                         cursor,
147                                                         user,
148                                                         user,
149                                                         ['name'],
150                                                         context
151                                                         )['name'],
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,
159      }
160     
161     _sql_constraints = [
162         (
163          'email_uniq',
164          'unique (email_id)',
165          'Another setting already exists with this email ID !')
166     ]
167
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)]
170
171     def _constraint_unique(self, cursor, user, ids):
172         """
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
177         """
178         if self.read(cursor, user, ids, ['company'])[0]['company'] == 'no':
179             accounts = self.search(cursor, user, [
180                                                  ('user', '=', user),
181                                                  ('company', '=', 'no')
182                                                  ])
183             if len(accounts) > 1 :
184                 return False
185             else :
186                 return True
187         else:
188             return True
189         
190     _constraints = [
191         (_constraint_unique,
192          'Error: You are not allowed to have more than 1 account.',
193          [])
194     ]
195
196     def get_outgoing_server(self, cursor, user, ids, context=None):
197         """
198         Returns the Out Going Connection (SMTP) object
199         
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
207         
208         @return: SMTP server object or Exception
209         """
210         #Type cast ids to integer
211         if type(ids) == list:
212             ids = ids[0]
213         this_object = self.browse(cursor, user, ids, context)
214         if this_object:
215             if this_object.smtpserver and this_object.smtpport: 
216                 try:
217                     if this_object.smtpssl:
218                         serv = smtplib.SMTP_SSL(this_object.smtpserver, this_object.smtpport)
219                     else:
220                         serv = smtplib.SMTP(this_object.smtpserver, this_object.smtpport)
221                     if this_object.smtptls:
222                         serv.ehlo()
223                         serv.starttls()
224                         serv.ehlo()
225                 except Exception, error:
226                     raise error
227                 try:
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:
231                     raise error
232                 return serv
233             raise Exception(_("SMTP SERVER or PORT not specified"))
234         raise Exception(_("Core connection for the given ID does not exist"))
235     
236     def check_outgoing_connection(self, cursor, user, ids, context=None):
237         """
238         checks SMTP credentials and confirms if outgoing connection works
239         (Attached to button)
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
245         """
246         try:
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
255                                  )
256     
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")
261
262     def smtp_connection(self, cursor, user, id, context=None):
263         """
264         This method should now wrap smtp_connection
265         """
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':
270             try:
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))
274                 return False
275             #Everything is complete, now return the connection
276             return serv
277         else:
278             logger.notifyChannel(_("Email Template"), netsvc.LOG_ERROR, _("Mail from Account %s failed. Probable Reason:Account not approved") % id)
279             return False
280                       
281 #**************************** MAIL SENDING FEATURES ***********************#
282
283
284
285     
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
288         if body is None:
289             body = {}
290         if payload is None:
291             payload = {}
292         if context is None:
293             context = {}
294         logger = netsvc.Logger()
295         for id in ids:  
296             core_obj = self.browse(cr, uid, id, context)
297             serv = self.smtp_connection(cr, uid, id)
298             if serv:
299                 try:
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)
307                     else:
308                         # otherwise a single multipart/mixed will do the whole job 
309                         payload_part = text_part = MIMEMultipart(_subtype=text_subtype)
310
311                     if subject:
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()
317                     else:
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]
330                     if message_id:
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', ''))
335                         if l == 0:
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>&lt;Empty Message&gt;</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'))
350
351                     #Now add attachments if any, wrapping into a container multipart/mixed if needed
352                     if payload:
353                         for file in payload:
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}
362                 try:
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
368                 serv.close()
369                 logger.notifyChannel(_("Email Template"), netsvc.LOG_INFO, _("Mail from Account %s successfully Sent.") % (id))
370                 return True
371             else:
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}
374
375     def extracttime(self, time_as_string):
376         """
377         TODO: DOC THis
378         """
379         logger = netsvc.Logger()
380         #The standard email dates are of format similar to:
381         #Thu, 8 Oct 2009 09:35:42 +0200
382         date_as_date = False
383         convertor = {'+':1, '-':-1}
384         try:
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)
390             else:
391                 sign = False
392             try:
393                 dt = datetime.datetime.strptime(
394                                             date_temp_str,
395                                             "%d %b %Y %H:%M:%S")
396             except:
397                 try:
398                     dt = datetime.datetime.strptime(
399                                             date_temp_str,
400                                             "%d %b %Y %H:%M")
401                 except:
402                     return False
403             if sign:
404                 try:
405                     offset = datetime.timedelta(
406                                 hours=sign * int(
407                                              date_list[5][1:3]
408                                                 ),
409                                              minutes=sign * int(
410                                                             date_list[5][3:5]
411                                                                 )
412                                                 )
413                 except Exception, e2:
414                     """Looks like UT or GMT, just forget decoding"""
415                     return False
416             else:
417                 offset = datetime.timedelta(hours=0)
418             dt = dt + offset
419             date_as_date = dt.strftime('%Y-%m-%d %H:%M:%S')
420         except Exception, e:
421             logger.notifyChannel(
422                     _("Email Template"),
423                     netsvc.LOG_WARNING,
424                     _(
425                       "Datetime Extraction failed.Date:%s \
426                       \tError:%s") % (
427                                     time_as_string,
428                                     e)
429                       )
430         return date_as_date
431         
432     def send_receive(self, cr, uid, ids, context=None):
433         for id in ids:
434             ctx = context.copy()
435             ctx['filters'] = [('account_id', '=', id)]
436             self.pool.get('email_template.mailbox').send_all_mail(cr, uid, [], context=ctx)
437         return True
438  
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
450         """
451         if not text:
452             return text        
453         p = re.compile("(=\?.*?\?.\?.*?\?=)")
454         text2 = ''
455         try:
456             for t2 in p.split(text):
457                 text2 += ''.join(
458                             [s.decode(
459                                       t or 'ascii'
460                                     ) for (s, t) in decode_header(t2)]
461                                 ).encode('utf-8')
462         except:
463             return text
464         return text2
465
466 email_template_account()
467
468 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: