[IMP] renaming message_state to message_unread
[odoo/odoo.git] / addons / mail / mail_mail.py
1
2 import ast
3 import base64
4 import email
5 import logging
6 import re
7 import time
8 import datetime
9
10 from osv import osv
11 from osv import fields
12
13 class mail_mail(osv.Model):
14     """
15     Model holding RFC2822 email messages to send. This model also provides
16     facilities to queue and send new email messages. 
17     """
18
19     _name = 'mail.mail'
20     _description = 'Outgoing Mails'
21     _inherits = {'mail.message': 'mail_message_id'}
22     _columns = {
23         'mail_message_id': fields.many2one('mail.message', 'Message', required=True, ondelete='cascade'),
24         'mail_server_id': fields.many2one('ir.mail_server', 'Outgoing mail server', readonly=1),
25         'subject': fields.char('Subject', size=128),
26         'state': fields.selection([
27             ('outgoing', 'Outgoing'),
28             ('sent', 'Sent'),
29             ('received', 'Received'),
30             ('exception', 'Delivery Failed'),
31             ('cancel', 'Cancelled'),
32         ], 'Status', readonly=True),
33         'auto_delete': fields.boolean('Auto Delete',
34             help="Permanently delete this email after sending it, to save space"),
35         'references': fields.text('References', help='Message references, such as identifiers of previous messages', readonly=1),
36         'email_from': fields.char('From', size=128, help='Message sender, taken from user preferences.'),
37         'email_to': fields.text('To', help='Message recipients'),
38         'email_cc': fields.char('Cc', size=256, help='Carbon copy message recipients'),
39         'reply_to':fields.char('Reply-To', size=256, help='Preferred response address for the message'),
40         'content_subtype': fields.char('Message content subtype', size=32,
41             oldname="subtype", readonly=1,
42             help="Type of message, usually 'html' or 'plain', used to select "\
43                   "plain-text or rich-text contents accordingly"),
44         'body_html': fields.html('Rich-text Contents', help="Rich-text/HTML version of the message"),
45     }
46
47     _defaults = {
48         'state': 'outgoing',
49         'content_subtype': 'plain',
50     }
51
52     def schedule_with_attach(self, cr, uid, email_from, email_to, subject, body, model=False, type='email',
53                              email_cc=None, reply_to=False, partner_ids=None, attachments=None,
54                              message_id=False, references=False, res_id=False, content_subtype='plain',
55                              headers=None, mail_server_id=False, auto_delete=False, context=None):
56         """ Schedule sending a new email message, to be sent the next time the
57             mail scheduler runs, or the next time :meth:`process_email_queue` is
58             called explicitly.
59
60             :param string email_from: sender email address
61             :param list email_to: list of recipient addresses (to be joined with commas) 
62             :param string subject: email subject (no pre-encoding/quoting necessary)
63             :param string body: email body, according to the ``content_subtype`` 
64                 (by default, plaintext). If html content_subtype is used, the
65                 message will be automatically converted to plaintext and wrapped
66                 in multipart/alternative.
67             :param list email_cc: optional list of string values for CC header
68                 (to be joined with commas)
69             :param string model: optional model name of the document this mail
70                 is related to (this will also be used to generate a tracking id,
71                 used to match any response related to the same document)
72             :param int res_id: optional resource identifier this mail is related
73                 to (this will also be used to generate a tracking id, used to
74                 match any response related to the same document)
75             :param string reply_to: optional value of Reply-To header
76             :param partner_ids: destination partner_ids
77             :param string content_subtype: optional mime content_subtype for
78                 the text body (usually 'plain' or 'html'), must match the format
79                 of the ``body`` parameter. Default is 'plain', making the content
80                 part of the mail "text/plain".
81             :param dict attachments: map of filename to filecontents, where
82                 filecontents is a string containing the bytes of the attachment
83             :param dict headers: optional map of headers to set on the outgoing
84                 mail (may override the other headers, including Subject,
85                 Reply-To, Message-Id, etc.)
86             :param int mail_server_id: optional id of the preferred outgoing
87                 mail server for this mail
88             :param bool auto_delete: optional flag to turn on auto-deletion of
89                 the message after it has been successfully sent (default to False)
90         """
91         if context is None:
92             context = {}
93         if attachments is None:
94             attachments = {}
95         if partner_ids is None:
96             partner_ids = []
97         attachment_obj = self.pool.get('ir.attachment')
98         for param in (email_to, email_cc):
99             if param and not isinstance(param, list):
100                 param = [param]
101         partner_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id.id
102         msg_vals = {
103                 'subject': subject,
104                 'date': fields.datetime.now(),
105                 'author_id': partner_id,
106                 'model': model,
107                 'res_id': res_id,
108                 'type': type,
109                 'body_text': body if content_subtype != 'html' else False,
110                 'body_html': body if content_subtype == 'html' else False,
111                 'email_from': email_from,
112                 'email_to': email_to and ','.join(email_to) or '',
113                 'email_cc': email_cc and ','.join(email_cc) or '',
114                 'partner_ids': partner_ids,
115                 'reply_to': reply_to,
116                 'message_id': message_id,
117                 'references': references,
118                 'content_subtype': content_subtype,
119                 'headers': headers, # serialize the dict on the fly
120                 'mail_server_id': mail_server_id,
121                 'state': 'outgoing',
122                 'auto_delete': auto_delete
123             }
124         email_msg_id = self.create(cr, uid, msg_vals, context)
125         msg = self.browse(cr, uid, email_msg_id, context)
126         for fname, fcontent in attachments.iteritems():
127             attachment_data = {
128                     'name': fname,
129                     'datas_fname': fname,
130                     'datas': fcontent and fcontent.encode('base64'),
131                     'res_model': 'mail.message',
132                     'res_id': msg.mail_message_id.id,
133             }
134             # FP Note: what's this ???
135             # if context.has_key('default_type'):
136             #     del context['default_type']
137         return email_msg_id
138
139     def mark_outgoing(self, cr, uid, ids, context=None):
140         return self.write(cr, uid, ids, {'state':'outgoing'}, context=context)
141
142     def cancel(self, cr, uid, ids, context=None):
143         return self.write(cr, uid, ids, {'state':'cancel'}, context=context)
144
145     def process_email_queue(self, cr, uid, ids=None, context=None):
146         """Send immediately queued messages, committing after each
147            message is sent - this is not transactional and should
148            not be called during another transaction!
149
150            :param list ids: optional list of emails ids to send. If passed
151                             no search is performed, and these ids are used
152                             instead.
153            :param dict context: if a 'filters' key is present in context,
154                                 this value will be used as an additional
155                                 filter to further restrict the outgoing
156                                 messages to send (by default all 'outgoing'
157                                 messages are sent).
158         """
159         if context is None:
160             context = {}
161         if not ids:
162             filters = ['&', ('state', '=', 'outgoing'), ('type', '=', 'email')]
163             if 'filters' in context:
164                 filters.extend(context['filters'])
165             ids = self.search(cr, uid, filters, context=context)
166         res = None
167         try:
168             # Force auto-commit - this is meant to be called by
169             # the scheduler, and we can't allow rolling back the status
170             # of previously sent emails!
171             res = self.send(cr, uid, ids, auto_commit=True, context=context)
172         except Exception:
173             _logger.exception("Failed processing mail queue")
174         return res
175
176     def _postprocess_sent_message(self, cr, uid, message, context=None):
177         """Perform any post-processing necessary after sending ``message``
178         successfully, including deleting it completely along with its
179         attachment if the ``auto_delete`` flag of the message was set.
180         Overridden by subclasses for extra post-processing behaviors. 
181
182         :param browse_record message: the message that was just sent
183         :return: True
184         """
185         if message.auto_delete:
186             self.pool.get('ir.attachment').unlink(cr, uid,
187                 [x.id for x in message.attachment_ids],
188                 context=context)
189             message.unlink()
190         return True
191
192     def send(self, cr, uid, ids, auto_commit=False, context=None):
193         """Sends the selected emails immediately, ignoring their current
194            state (mails that have already been sent should not be passed
195            unless they should actually be re-sent).
196            Emails successfully delivered are marked as 'sent', and those
197            that fail to be deliver are marked as 'exception', and the
198            corresponding error message is output in the server logs.
199
200            :param bool auto_commit: whether to force a commit of the message
201                                     status after sending each message (meant
202                                     only for processing by the scheduler),
203                                     should never be True during normal
204                                     transactions (default: False)
205            :return: True
206         """
207         ir_mail_server = self.pool.get('ir.mail_server')
208         self.write(cr, uid, ids, {'state': 'outgoing'}, context=context)
209         for message in self.browse(cr, uid, ids, context=context):
210             try:
211                 attachments = []
212                 for attach in message.attachment_ids:
213                     attachments.append((attach.datas_fname, base64.b64decode(attach.datas)))
214
215                 body = message.body_html if message.content_subtype == 'html' else message.body_text
216                 body_alternative = None
217                 content_subtype_alternative = None
218                 if message.content_subtype == 'html' and message.body_text:
219                     # we have a plain text alternative prepared, pass it to 
220                     # build_message instead of letting it build one
221                     body_alternative = message.body_text
222                     content_subtype_alternative = 'plain'
223
224                 # handle destination_partners
225                 partner_ids_email_to = ''
226                 for partner in message.partner_ids:
227                     partner_ids_email_to += '%s ' % (partner.email or '')
228                 message_email_to = '%s %s' % (partner_ids_email_to, message.email_to or '')
229
230                 # build an RFC2822 email.message.Message object and send it
231                 # without queuing
232                 msg = ir_mail_server.build_email(
233                     email_from=message.email_from,
234                     email_to=mail_tools_to_email(message_email_to),
235                     subject=message.subject,
236                     body=body,
237                     body_alternative=body_alternative,
238                     email_cc=mail_tools_to_email(message.email_cc),
239                     reply_to=message.reply_to,
240                     attachments=attachments, message_id=message.message_id,
241                     references = message.references,
242                     object_id=message.res_id and ('%s-%s' % (message.res_id,message.model)),
243                     subtype=message.content_subtype,
244                     subtype_alternative=content_subtype_alternative,
245                     headers=message.headers and ast.literal_eval(message.headers))
246                 res = ir_mail_server.send_email(cr, uid, msg,
247                                                 mail_server_id=message.mail_server_id.id,
248                                                 context=context)
249                 if res:
250                     message.write({'state':'sent', 'message_id': res, 'email_to': message_email_to})
251                 else:
252                     message.write({'state':'exception', 'email_to': message_email_to})
253                 message.refresh()
254                 if message.state == 'sent':
255                     self._postprocess_sent_message(cr, uid, message, context=context)
256             except Exception:
257                 _logger.exception('failed sending mail.message %s', message.id)
258                 message.write({'state':'exception'})
259
260             if auto_commit == True:
261                 cr.commit()
262         return True
263