11 from osv import fields
13 class mail_mail(osv.Model):
15 Model holding RFC2822 email messages to send. This model also provides
16 facilities to queue and send new email messages.
20 _description = 'Outgoing Mails'
21 _inherits = {'mail.message': 'mail_message_id'}
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'),
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"),
49 'content_subtype': 'plain',
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
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)
93 if attachments is None:
95 if partner_ids is None:
97 attachment_obj = self.pool.get('ir.attachment')
98 for param in (email_to, email_cc):
99 if param and not isinstance(param, list):
101 partner_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id.id
104 'date': fields.datetime.now(),
105 'author_id': partner_id,
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,
122 'auto_delete': auto_delete
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():
129 'datas_fname': fname,
130 'datas': fcontent and fcontent.encode('base64'),
131 'res_model': 'mail.message',
132 'res_id': msg.mail_message_id.id,
134 # FP Note: what's this ???
135 # if context.has_key('default_type'):
136 # del context['default_type']
139 def mark_outgoing(self, cr, uid, ids, context=None):
140 return self.write(cr, uid, ids, {'state':'outgoing'}, context=context)
142 def cancel(self, cr, uid, ids, context=None):
143 return self.write(cr, uid, ids, {'state':'cancel'}, context=context)
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!
150 :param list ids: optional list of emails ids to send. If passed
151 no search is performed, and these ids are used
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'
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)
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)
173 _logger.exception("Failed processing mail queue")
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.
182 :param browse_record message: the message that was just sent
185 if message.auto_delete:
186 self.pool.get('ir.attachment').unlink(cr, uid,
187 [x.id for x in message.attachment_ids],
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.
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)
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):
212 for attach in message.attachment_ids:
213 attachments.append((attach.datas_fname, base64.b64decode(attach.datas)))
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'
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 '')
230 # build an RFC2822 email.message.Message object and send it
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,
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,
250 message.write({'state':'sent', 'message_id': res, 'email_to': message_email_to})
252 message.write({'state':'exception', 'email_to': message_email_to})
254 if message.state == 'sent':
255 self._postprocess_sent_message(cr, uid, message, context=context)
257 _logger.exception('failed sending mail.message %s', message.id)
258 message.write({'state':'exception'})
260 if auto_commit == True: