[NEW] email_template (extracted from poweremail) just to send emails -specially for...
[odoo/odoo.git] / addons / email_template / email_template_account.py
1 from osv import osv, fields
2 from html2text import html2text
3 import re
4 import smtplib
5 import base64
6 from email import Encoders
7 from email.mime.base import MIMEBase
8 from email.mime.multipart import MIMEMultipart
9 from email.mime.text import MIMEText
10 from email.header import decode_header, Header
11 from email.utils import formatdate
12 import re
13 import netsvc
14 import string
15 import email
16 import time, datetime
17 import email_template_engines
18 from tools.translate import _
19 import tools
20
21 class email_template_account(osv.osv):
22     """
23     Object to store email account settings
24     """
25     _name = "email_template.account"
26     _known_content_types = ['multipart/mixed',
27                             'multipart/alternative',
28                             'multipart/related',
29                             'text/plain',
30                             'text/html'
31                             ]
32     _columns = {
33         'name': fields.char('Email Account Desc',
34                         size=64, required=True,
35                         readonly=True, select=True,
36                         states={'draft':[('readonly', False)]}),
37         'user':fields.many2one('res.users',
38                         'Related User', required=True,
39                         readonly=True, states={'draft':[('readonly', False)]}),
40         'email_id': fields.char('Email ID',
41                         size=120, required=True,
42                         readonly=True, states={'draft':[('readonly', False)]} ,
43                         help=" eg:yourname@yourdomain.com "),
44         'smtpserver': fields.char('Server',
45                         size=120, required=True,
46                         readonly=True, states={'draft':[('readonly', False)]},
47                         help="Enter name of outgoing server,eg:smtp.gmail.com "),
48         'smtpport': fields.integer('SMTP Port ',
49                         size=64, required=True,
50                         readonly=True, states={'draft':[('readonly', False)]},
51                         help="Enter port number,eg:SMTP-587 "),
52         'smtpuname': fields.char('User Name',
53                         size=120, required=False,
54                         readonly=True, states={'draft':[('readonly', False)]}),
55         'smtppass': fields.char('Password',
56                         size=120, invisible=True,
57                         required=False, readonly=True,
58                         states={'draft':[('readonly', False)]}),
59         'smtptls':fields.boolean('Use TLS',
60                         states={'draft':[('readonly', False)]}, readonly=True),
61                                 
62         'smtpssl':fields.boolean('Use SSL/TLS (only in python 2.6)',
63                         states={'draft':[('readonly', False)]}, readonly=True),
64         'send_pref':fields.selection([
65                                       ('html', 'HTML otherwise Text'),
66                                       ('text', 'Text otherwise HTML'),
67                                       ('both', 'Both HTML & Text')
68                                       ], 'Mail Format', required=True),
69         'allowed_groups':fields.many2many(
70                         'res.groups',
71                         'account_group_rel', 'templ_id', 'group_id',
72                         string="Allowed User Groups",
73                         help="Only users from these groups will be" \
74                         "allowed to send mails from this ID"),
75         'company':fields.selection([
76                         ('yes', 'Yes'),
77                         ('no', 'No')
78                         ], 'Company Mail A/c',
79                         readonly=True,
80                         help="Select if this mail account does not belong" \
81                         "to specific user but the organisation as a whole." \
82                         "eg:info@somedomain.com",
83                         required=True, states={
84                                            'draft':[('readonly', False)]
85                                            }),
86
87         'state':fields.selection([
88                                   ('draft', 'Initiated'),
89                                   ('suspended', 'Suspended'),
90                                   ('approved', 'Approved')
91                                   ],
92                         'Account Status', required=True, readonly=True),
93     }
94
95     _defaults = {
96          'name':lambda self, cursor, user, context:self.pool.get(
97                                                 'res.users'
98                                                 ).read(
99                                                         cursor,
100                                                         user,
101                                                         user,
102                                                         ['name'],
103                                                         context
104                                                         )['name'],
105          'smtpssl':lambda * a:True,
106          'state':lambda * a:'draft',
107          'user':lambda self, cursor, user, context:user,
108          'send_pref':lambda * a: 'html',
109          'smtptls':lambda * a:True,
110      }
111     
112     _sql_constraints = [
113         (
114          'email_uniq',
115          'unique (email_id)',
116          'Another setting already exists with this email ID !')
117     ]
118     
119     def _constraint_unique(self, cursor, user, ids):
120         """
121         This makes sure that you dont give personal 
122         users two accounts with same ID (Validated in sql constaints)
123         However this constraint exempts company accounts. 
124         Any no of co accounts for a user is allowed
125         """
126         if self.read(cursor, user, ids, ['company'])[0]['company'] == 'no':
127             accounts = self.search(cursor, user, [
128                                                  ('user', '=', user),
129                                                  ('company', '=', 'no')
130                                                  ])
131             if len(accounts) > 1 :
132                 return False
133             else :
134                 return True
135         else:
136             return True
137         
138     _constraints = [
139         (_constraint_unique,
140          'Error: You are not allowed to have more than 1 account.',
141          [])
142     ]
143     
144     def on_change_emailid(self, cursor, user, ids, name=None, email_id=None, context=None):
145         """
146         Called when the email ID field changes.
147         
148         UI enhancement
149         Writes the same email value to the smtpusername
150         and incoming username
151         """
152         #TODO: Check and remove the write. Is it needed?
153         self.write(cursor, user, ids, {'state':'draft'}, context=context)
154         return {
155                 'value': {
156                           'state': 'draft',
157                           'smtpuname':email_id,
158                           'isuser':email_id
159                           }
160                 }
161     
162     def get_outgoing_server(self, cursor, user, ids, context=None):
163         """
164         Returns the Out Going Connection (SMTP) object
165         
166         @attention: DO NOT USE except_osv IN THIS METHOD
167         @param cursor: Database Cursor
168         @param user: ID of current user
169         @param ids: ID/list of ids of current object for 
170                     which connection is required
171                     First ID will be chosen from lists
172         @param context: Context
173         
174         @return: SMTP server object or Exception
175         """
176         #Type cast ids to integer
177         if type(ids) == list:
178             ids = ids[0]
179         this_object = self.browse(cursor, user, ids, context)
180         if this_object:
181             if this_object.smtpserver and this_object.smtpport: 
182                 try:
183                     if this_object.smtpssl:
184                         serv = smtplib.SMTP_SSL(this_object.smtpserver, this_object.smtpport)
185                     else:
186                         serv = smtplib.SMTP(this_object.smtpserver, this_object.smtpport)
187                     if this_object.smtptls:
188                         serv.ehlo()
189                         serv.starttls()
190                         serv.ehlo()
191                 except Exception, error:
192                     raise error
193                 try:
194                     if serv.has_extn('AUTH') or this_object.smtpuname or this_object.smtppass:
195                         serv.login(this_object.smtpuname, this_object.smtppass)
196                 except Exception, error:
197                     raise error
198                 return serv
199             raise Exception(_("SMTP SERVER or PORT not specified"))
200         raise Exception(_("Core connection for the given ID does not exist"))
201     
202     def check_outgoing_connection(self, cursor, user, ids, context=None):
203         """
204         checks SMTP credentials and confirms if outgoing connection works
205         (Attached to button)
206         @param cursor: Database Cursor
207         @param user: ID of current user
208         @param ids: list of ids of current object for 
209                     which connection is required
210         @param context: Context
211         """
212         try:
213             self.get_outgoing_server(cursor, user, ids, context)
214             raise osv.except_osv(_("SMTP Test Connection Was Successful"), '')
215         except osv.except_osv, success_message:
216             raise success_message
217         except Exception, error:
218             raise osv.except_osv(
219                                  _("Out going connection test failed"),
220                                  _("Reason: %s") % error
221                                  )
222     
223     def do_approval(self, cr, uid, ids, context={}):
224         #TODO: Check if user has rights
225         self.write(cr, uid, ids, {'state':'approved'}, context=context)
226 #        wf_service = netsvc.LocalService("workflow")
227
228     def smtp_connection(self, cursor, user, id, context=None):
229         """
230         This method should now wrap smtp_connection
231         """
232         #This function returns a SMTP server object
233         logger = netsvc.Logger()
234         core_obj = self.browse(cursor, user, id, context)
235         if core_obj.smtpserver and core_obj.smtpport and core_obj.state == 'approved':
236             try:
237                 serv = self.get_outgoing_server(cursor, user, id, context)
238             except Exception, error:
239                 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))
240                 return False
241             #Everything is complete, now return the connection
242             return serv
243         else:
244             logger.notifyChannel(_("Email Template"), netsvc.LOG_ERROR, _("Mail from Account %s failed. Probable Reason:Account not approved") % id)
245             return False
246                       
247 #**************************** MAIL SENDING FEATURES ***********************#
248     def split_to_ids(self, ids_as_str):
249         """
250         Identifies email IDs separated by separators
251         and returns a list
252         TODO: Doc this
253         "a@b.com,c@bcom; d@b.com;e@b.com->['a@b.com',...]"
254         """
255         email_sep_by_commas = ids_as_str \
256                                     .replace('; ', ',') \
257                                     .replace(';', ',') \
258                                     .replace(', ', ',')
259         return email_sep_by_commas.split(',')
260     
261     def get_ids_from_dict(self, addresses={}):
262         """
263         TODO: Doc this
264         """
265         result = {'all':[]}
266         keys = ['To', 'CC', 'BCC']
267         for each in keys:
268             ids_as_list = self.split_to_ids(addresses.get(each, u''))
269             while u'' in ids_as_list:
270                 ids_as_list.remove(u'')
271             result[each] = ids_as_list
272             result['all'].extend(ids_as_list)
273         return result
274     
275     def send_mail(self, cr, uid, ids, addresses, subject='', body=None, payload=None, context=None):
276         #TODO: Replace all this crap with a single email object
277         if body is None:
278             body = {}
279         if payload is None:
280             payload = {}
281         if context is None:
282             context = {}
283         logger = netsvc.Logger()
284         for id in ids:  
285             core_obj = self.browse(cr, uid, id, context)
286             serv = self.smtp_connection(cr, uid, id)
287             if serv:
288                 try:
289                     msg = MIMEMultipart()
290                     if subject:
291                         msg['Subject'] = subject
292                     sender_name = Header(core_obj.name, 'utf-8').encode()
293                     msg['From'] = sender_name + " <" + core_obj.email_id + ">"
294                     msg['Organization'] = tools.ustr(core_obj.user.company_id.name)
295                     msg['Date'] = formatdate()
296                     addresses_l = self.get_ids_from_dict(addresses) 
297                     if addresses_l['To']:
298                         msg['To'] = u','.join(addresses_l['To'])
299                     if addresses_l['CC']:
300                         msg['CC'] = u','.join(addresses_l['CC'])
301 #                    if addresses_l['BCC']:
302 #                        msg['BCC'] = u','.join(addresses_l['BCC'])
303                     if body.get('text', False):
304                         temp_body_text = body.get('text', '')
305                         l = len(temp_body_text.replace(' ', '').replace('\r', '').replace('\n', ''))
306                         if l == 0:
307                             body['text'] = u'No Mail Message'
308                     # Attach parts into message container.
309                     # According to RFC 2046, the last part of a multipart message, in this case
310                     # the HTML message, is best and preferred.
311                     if core_obj.send_pref == 'text' or core_obj.send_pref == 'both':
312                         body_text = body.get('text', u'No Mail Message')
313                         body_text = tools.ustr(body_text)
314                         msg.attach(MIMEText(body_text.encode("utf-8"), _charset='UTF-8'))
315                     if core_obj.send_pref == 'html' or core_obj.send_pref == 'both':
316                         html_body = body.get('html', u'')
317                         if len(html_body) == 0 or html_body == u'':
318                             html_body = body.get('text', u'<p>No Mail Message</p>').replace('\n', '<br/>').replace('\r', '<br/>')
319                         html_body = tools.ustr(html_body)
320                         msg.attach(MIMEText(html_body.encode("utf-8"), _subtype='html', _charset='UTF-8'))
321                     #Now add attachments if any
322                     for file in payload.keys():
323                         part = MIMEBase('application', "octet-stream")
324                         part.set_payload(base64.decodestring(payload[file]))
325                         part.add_header('Content-Disposition', 'attachment; filename="%s"' % file)
326                         Encoders.encode_base64(part)
327                         msg.attach(part)
328                 except Exception, error:
329                     logger.notifyChannel(_("Email Template"), netsvc.LOG_ERROR, _("Mail from Account %s failed. Probable Reason:MIME Error\nDescription: %s") % (id, error))
330                     return error
331                 try:
332                     #print msg['From'],toadds
333                     serv.sendmail(msg['From'], addresses_l['all'], msg.as_string())
334                 except Exception, error:
335                     logger.notifyChannel(_("Email Template"), netsvc.LOG_ERROR, _("Mail from Account %s failed. Probable Reason:Server Send Error\nDescription: %s") % (id, error))
336                     return error
337                 #The mail sending is complete
338                 serv.close()
339                 logger.notifyChannel(_("Email Template"), netsvc.LOG_INFO, _("Mail from Account %s successfully Sent.") % (id))
340                 return True
341             else:
342                 logger.notifyChannel(_("Email Template"), netsvc.LOG_ERROR, _("Mail from Account %s failed. Probable Reason:Account not approved") % id)
343                                 
344     def extracttime(self, time_as_string):
345         """
346         TODO: DOC THis
347         """
348         logger = netsvc.Logger()
349         #The standard email dates are of format similar to:
350         #Thu, 8 Oct 2009 09:35:42 +0200
351         #print time_as_string
352         date_as_date = False
353         convertor = {'+':1, '-':-1}
354         try:
355             time_as_string = time_as_string.replace(',', '')
356             date_list = time_as_string.split(' ')
357             date_temp_str = ' '.join(date_list[1:5])
358             if len(date_list) >= 6:
359                 sign = convertor.get(date_list[5][0], False)
360             else:
361                 sign = False
362             try:
363                 dt = datetime.datetime.strptime(
364                                             date_temp_str,
365                                             "%d %b %Y %H:%M:%S")
366             except:
367                 try:
368                     dt = datetime.datetime.strptime(
369                                             date_temp_str,
370                                             "%d %b %Y %H:%M")
371                 except:
372                     return False
373             if sign:
374                 try:
375                     offset = datetime.timedelta(
376                                 hours=sign * int(
377                                              date_list[5][1:3]
378                                                 ),
379                                              minutes=sign * int(
380                                                             date_list[5][3:5]
381                                                                 )
382                                                 )
383                 except Exception, e2:
384                     """Looks like UT or GMT, just forget decoding"""
385                     return False
386             else:
387                 offset = datetime.timedelta(hours=0)
388             dt = dt + offset
389             date_as_date = dt.strftime('%Y-%m-%d %H:%M:%S')
390             #print date_as_date
391         except Exception, e:
392             logger.notifyChannel(
393                     _("Email Template"),
394                     netsvc.LOG_WARNING,
395                     _(
396                       "Datetime Extraction failed.Date:%s \
397                       \tError:%s") % (
398                                     time_as_string,
399                                     e)
400                       )
401         return date_as_date
402         
403     def send_receive(self, cr, uid, ids, context=None):
404         self.get_mails(cr, uid, ids, context)
405         for id in ids:
406             ctx = context.copy()
407             ctx['filters'] = [('account_id', '=', id)]
408             self.pool.get('email_template.mailbox').send_all_mail(cr, uid, [], context=ctx)
409         return True
410  
411     def decode_header_text(self, text):
412         """ Decode internationalized headers RFC2822.
413             To, CC, BCC, Subject fields can contain 
414             text slices with different encodes, like:
415                 =?iso-8859-1?Q?Enric_Mart=ED?= <enricmarti@company.com>, 
416                 =?Windows-1252?Q?David_G=F3mez?= <david@company.com>
417             Sometimes they include extra " character at the beginning/
418             end of the contact name, like:
419                 "=?iso-8859-1?Q?Enric_Mart=ED?=" <enricmarti@company.com>
420             and decode_header() does not work well, so we use regular 
421             expressions (?=   ? ?   ?=) to split the text slices
422         """
423         if not text:
424             return text        
425         p = re.compile("(=\?.*?\?.\?.*?\?=)")
426         text2 = ''
427         try:
428             for t2 in p.split(text):
429                 text2 += ''.join(
430                             [s.decode(
431                                       t or 'ascii'
432                                     ) for (s, t) in decode_header(t2)]
433                                 ).encode('utf-8')
434         except:
435             return text
436         return text2
437
438 email_template_account()
439
440 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: